[Scummvm-git-logs] scummvm master -> e0b8712b63e2f97896bb92ce2a7520c0b9975b1a

mgerhardy noreply at scummvm.org
Sun Jul 5 20:12:08 UTC 2026


This automated email contains information about 3 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
ede8e9237f MACS2: add binding for help menu
913b66dbec MACS2: helper methods
e0b8712b63 MACS2: implemented scumm verb ui hidden behind an enhancement


Commit: ede8e9237fae563cbce035910edd1e8a403a9157
    https://github.com/scummvm/scummvm/commit/ede8e9237fae563cbce035910edd1e8a403a9157
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-07-05T22:11:43+02:00

Commit Message:
MACS2: add binding for help menu

Changed paths:
    engines/macs2/detection.h
    engines/macs2/metaengine.cpp
    engines/macs2/view1.cpp
    engines/macs2/view1.h


diff --git a/engines/macs2/detection.h b/engines/macs2/detection.h
index cf807d965a3..52f60774382 100644
--- a/engines/macs2/detection.h
+++ b/engines/macs2/detection.h
@@ -44,6 +44,7 @@ enum Macs2Action {
 	kMacs2ActionMenu,
 	kMacs2ActionGameSpeed,
 	kMacs2ActionOpenGMM,
+	kMacs2ActionHelp,
 };
 
 extern const PlainGameDescriptor macs2Games[];
diff --git a/engines/macs2/metaengine.cpp b/engines/macs2/metaengine.cpp
index 390c94e9780..3be0bda1883 100644
--- a/engines/macs2/metaengine.cpp
+++ b/engines/macs2/metaengine.cpp
@@ -82,6 +82,11 @@ Common::KeymapArray Macs2MetaEngine::initKeymaps(const char *target) const {
 	act->addDefaultInputMapping("F5");
 	engineKeyMap->addAction(act);
 
+	act = new Action("HELP", _("Help / map"));
+	act->setCustomEngineActionEvent(kMacs2ActionHelp);
+	act->addDefaultInputMapping("F1");
+	engineKeyMap->addAction(act);
+
 	return Keymap::arrayOf(engineKeyMap);
 }
 
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 280a8d6d5dc..0417d9962f4 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -263,7 +263,7 @@ int View1::findInventoryItem(const GameObject *item) {
 	return -1;
 }
 
-Character *View1::getCharacterByIndex(uint16 index) {
+Character *View1::getCharacterByIndex(uint16 index) const {
 	if (index > 0 && index <= kMaxSceneObjects) {
 		Character *c = _characterByObjectIndex[index];
 		if (c != nullptr && c->_gameObject != nullptr && c->_gameObject->_index == index)
@@ -1824,6 +1824,13 @@ bool View1::msgAction(const ActionMessage &msg) {
 		g_engine->openMainMenuDialog();
 		updateCursor();
 		return true;
+	case Macs2::kMacs2ActionHelp:
+		if (!_helpButtonDisabled && _currentMode != ViewMode::VM_HELP &&
+			!g_engine->_scriptExecutor->isExecuting() &&
+			g_engine->_scriptExecutor->_cursorMode != Script::MouseMode::Disabled) {
+			enterMapMode();
+		}
+		return true;
 	default:
 		break;
 	}
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index 37faf599802..3ee9e392997 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -481,7 +481,7 @@ public:
 
 	void transferInventoryItem(GameObject *item, GameObject *targetContainer);
 
-	Character *getCharacterByIndex(uint16 index);
+	Character *getCharacterByIndex(uint16 index) const;
 	void rebuildCharacterLookupTable() const;
 
 	int getCharacterArrayIndex(const Character *c) const;


Commit: 913b66dbec34842a0f56d97efad8cc1d82a33043
    https://github.com/scummvm/scummvm/commit/913b66dbec34842a0f56d97efad8cc1d82a33043
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-07-05T22:11:43+02:00

Commit Message:
MACS2: helper methods

Changed paths:
    engines/macs2/view1.cpp
    engines/macs2/view1.h


diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 0417d9962f4..c40149c5c7d 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -510,6 +510,30 @@ void View1::renderString(const Common::Point pos, const Common::String &s) {
 	renderString(pos.x, pos.y, s);
 }
 
+void View1::renderStringTo(uint16 x, uint16 y, const Common::String &s, Graphics::ManagedSurface &surf) {
+	uint16 currentX = x;
+	uint16 currentY = y;
+
+	uint16 widestGlyph = 1;
+	for (auto iter = s.begin(); iter != s.end(); iter++) {
+		GlyphData data;
+		if (g_engine->findGlyph(*iter, data)) {
+			widestGlyph = MAX(widestGlyph, data._width);
+		}
+	}
+
+	for (auto iter = s.begin(); iter != s.end(); iter++) {
+		GlyphData data;
+		const bool found = g_engine->findGlyph(*iter, data);
+		if (found) {
+			drawSprite(currentX, currentY, data, surf, false);
+			currentX += data._width + 1;
+		} else {
+			currentX += widestGlyph;
+		}
+	}
+}
+
 int View1::measureStringWithFont(const Common::String &s, const GlyphData *glyphs, uint16 numGlyphs) {
 	int width = 0;
 	uint16 widestGlyph = 1;
@@ -533,6 +557,11 @@ int View1::measureStringWithFont(const Common::String &s, const GlyphData *glyph
 
 void View1::renderStringWithFont(uint16 x, uint16 y, const Common::String &s, const GlyphData *glyphs, uint16 numGlyphs) {
 	Graphics::ManagedSurface surf = getSurface();
+	renderStringWithFontTo(x, y, s, glyphs, numGlyphs, surf);
+}
+
+void View1::renderStringWithFontTo(uint16 x, uint16 y, const Common::String &s, const GlyphData *glyphs,
+								   uint16 numGlyphs, Graphics::ManagedSurface &surf) {
 	uint16 currentX = x;
 	uint16 widestGlyph = 1;
 	for (uint i = 0; i < numGlyphs; i++) {
@@ -2686,6 +2715,49 @@ void View1::drawSpriteClipped(uint16 x, uint16 y, Common::Rect &clippingRect, co
 	drawSpriteClipped(x, y, clippingRect, sprite._width, sprite._height, sprite._data.data(), s);
 }
 
+void View1::drawSpriteFitted(const Common::Rect &bounds, const Sprite &sprite, Graphics::ManagedSurface &s, uint16 inset) {
+	if (sprite._width == 0 || sprite._height == 0 || sprite._data.empty())
+		return;
+
+	const Common::Rect inner(bounds.left + inset, bounds.top + inset,
+							 bounds.right - inset, bounds.bottom - inset);
+	if (inner.width() <= 0 || inner.height() <= 0)
+		return;
+
+	const int destW = inner.width();
+	const int destH = inner.height();
+	const int scaleW = (destW * 256) / sprite._width;
+	const int scaleH = (destH * 256) / sprite._height;
+	const int scale = MIN(scaleW, scaleH);
+	if (scale <= 0)
+		return;
+
+	const int drawW = MAX(1, (sprite._width * scale) / 256);
+	const int drawH = MAX(1, (sprite._height * scale) / 256);
+	const int startX = inner.left + (destW - drawW) / 2;
+	const int startY = inner.top + (destH - drawH) / 2;
+
+	for (int dy = 0; dy < drawH; dy++) {
+		const int srcY = (dy * sprite._height) / drawH;
+		const int py = startY + dy;
+		if (py < inner.top || py >= inner.bottom)
+			continue;
+
+		for (int dx = 0; dx < drawW; dx++) {
+			const int srcX = (dx * sprite._width) / drawW;
+			const uint8 val = sprite._data[srcY * sprite._width + srcX];
+			if (val == 0)
+				continue;
+
+			const int px = startX + dx;
+			if (px < inner.left || px >= inner.right || px < 0 || px >= s.w || py < 0 || py >= s.h)
+				continue;
+
+			s.setPixel(px, py, val);
+		}
+	}
+}
+
 static byte applyShadingTable(byte color, int shadingTableOffset) {
 	if (shadingTableOffset == 0)
 		return color;
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index 3ee9e392997..41538be1134 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -306,7 +306,10 @@ private:
 
 	void renderString(uint16 x, uint16 y, const Common::String &s);
 	void renderString(const Common::Point pos, const Common::String &s);
+	void renderStringTo(uint16 x, uint16 y, const Common::String &s, Graphics::ManagedSurface &surf);
 	void renderStringWithFont(uint16 x, uint16 y, const Common::String &s, const GlyphData *glyphs, uint16 numGlyphs);
+	void renderStringWithFontTo(uint16 x, uint16 y, const Common::String &s, const GlyphData *glyphs,
+								uint16 numGlyphs, Graphics::ManagedSurface &surf);
 	int measureStringWithFont(const Common::String &s, const GlyphData *glyphs, uint16 numGlyphs);
 
 	Common::Array<Common::Rect> _mainMenuButtonLocations;
@@ -322,6 +325,7 @@ private:
 
 	void drawSpriteClipped(uint16 x, uint16 y, Common::Rect &clippingRect, uint16 width, uint16 height, const byte *const data, Graphics::ManagedSurface &s);
 	void drawSpriteClipped(uint16 x, uint16 y, Common::Rect &clippingRect, const Sprite &sprite, Graphics::ManagedSurface &s);
+	void drawSpriteFitted(const Common::Rect &bounds, const Sprite &sprite, Graphics::ManagedSurface &s, uint16 inset = 6);
 
 	// Binary sortObjectListByY (1008:8cf2) + buildSortedObjectList (1008:8c5a):
 	// scan 512 objects, collect current-scene indices at 0xFAC, quicksort by Y.


Commit: e0b8712b63e2f97896bb92ce2a7520c0b9975b1a
    https://github.com/scummvm/scummvm/commit/e0b8712b63e2f97896bb92ce2a7520c0b9975b1a
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-07-05T22:11:43+02:00

Commit Message:
MACS2: implemented scumm verb ui hidden behind an enhancement

Changed paths:
  A engines/macs2/scummui.cpp
  A engines/macs2/scummui.h
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/macs2_constants.h
    engines/macs2/module.mk
    engines/macs2/saveload.cpp
    engines/macs2/view1.cpp
    engines/macs2/view1.h


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index c335eb739c4..bdfd2e59636 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -33,6 +33,7 @@
 #include "common/types.h"
 #include "common/util.h"
 #include "engines/util.h"
+#include "engines/enhancements.h"
 #include "gameobjects.h"
 #include "graphics/cursorman.h"
 #include "graphics/paletteman.h"
@@ -1119,6 +1120,23 @@ int Macs2Engine::computeMinCostToReachable(int nodeIndex, int prevNode, uint16 a
 	return result;
 }
 
+void Macs2Engine::nextCursorMode() {
+	switch (_scriptExecutor->_cursorMode) {
+	case Script::MouseMode::Talk:
+		setCursorMode(Script::MouseMode::Look);
+		break;
+	case Script::MouseMode::Look:
+		setCursorMode(Script::MouseMode::Use);
+		break;
+	case Script::MouseMode::Use:
+		setCursorMode(Script::MouseMode::Walk);
+		break;
+	default:
+		setCursorMode(Script::MouseMode::Talk);
+		break;
+	}
+}
+
 void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
 	// setCursorMode (1008:3ea5): when the cursor image changes, keep the hotspot
 	// fixed on screen by compensating for the old/new image half-extents, clamp,
@@ -1135,26 +1153,42 @@ void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
 		halfH = _imageResources[index]._height / 2;
 	};
 
+	auto isGameplayVerb = [](Script::MouseMode mode) {
+		return mode == Script::MouseMode::Talk || mode == Script::MouseMode::Look ||
+			   mode == Script::MouseMode::Use || mode == Script::MouseMode::Walk;
+	};
+
+	View1 *view = (View1 *)findView("View1");
+	const bool scummVerbUI = view && view->hasScummVerbUI();
+
 	uint16 oldHalfW = 0, oldHalfH = 0, newHalfW = 0, newHalfH = 0;
 	cursorHalfSize(oldMode, oldHalfW, oldHalfH);
 
 	Common::Point mouse = g_system->getEventManager()->getMousePos();
-	mouse.x += oldHalfW;
-	mouse.y += oldHalfH;
+	const bool mouseInUiPanel = scummVerbUI && mouse.y >= kGameHeight;
 
 	_scriptExecutor->_cursorMode = newMode;
 
-	cursorHalfSize(newMode, newHalfW, newHalfH);
-	mouse.x -= newHalfW;
-	mouse.y -= newHalfH;
+	// Keep the pointer on the verb/inventory panel when selecting verbs there, and
+	// skip hotspot compensation when the SCUMM UI shows the same walk cursor for all verbs.
+	if (!mouseInUiPanel && !(scummVerbUI && isGameplayVerb(oldMode) && isGameplayVerb(newMode))) {
+		mouse.x += oldHalfW;
+		mouse.y += oldHalfH;
 
-	mouse.x = CLIP<int>(mouse.x, (int)newHalfW, kScreenWidthLast - (int)newHalfW);
-	mouse.y = CLIP<int>(mouse.y, (int)newHalfH, kGameHeightLast - (int)newHalfH);
-	g_system->warpMouse(mouse.x, mouse.y);
+		cursorHalfSize(newMode, newHalfW, newHalfH);
+		mouse.x -= newHalfW;
+		mouse.y -= newHalfH;
+
+		const int maxY = scummVerbUI ? (kScreenHeightLast - (int)newHalfH)
+									: (kGameHeightLast - (int)newHalfH);
+		mouse.x = CLIP<int>(mouse.x, (int)newHalfW, kScreenWidthLast - (int)newHalfW);
+		mouse.y = CLIP<int>(mouse.y, (int)newHalfH, maxY);
+		g_system->warpMouse(mouse.x, mouse.y);
+	}
 
 	_clipRectDirty = true;
 
-	if (View1 *view = (View1 *)findView("View1"))
+	if (view)
 		view->updateCursor();
 
 	if (cursorVisible)
@@ -1760,8 +1794,8 @@ Common::Error Macs2Engine::run() {
 		loadTranslation();
 	}
 
-	// Initialize graphics mode
-	initGraphics(kScreenWidth, kGameHeight);
+	// Initialize graphics mode (taller framebuffer when SCUMM verb UI is enabled)
+	initGraphics(kScreenWidth, enhancementEnabled(kEnhUIUX) ? kScreenHeight : kGameHeight);
 
 	CursorMan.showMouse(false);
 
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 0306562bcd6..f8831e33f6a 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -369,6 +369,7 @@ public:
 	Common::MemoryReadStream *_fileStream;
 
 	void setCursorMode(Script::MouseMode newMode);
+	void nextCursorMode();
 
 	Common::Array<uint16> _hotspotColorTable;
 
diff --git a/engines/macs2/macs2_constants.h b/engines/macs2/macs2_constants.h
index e2869b63520..bc707dc8051 100644
--- a/engines/macs2/macs2_constants.h
+++ b/engines/macs2/macs2_constants.h
@@ -30,6 +30,11 @@ static constexpr int kScreenWidthLast = kScreenWidth - 1;
 static constexpr int kGameHeight      = 200;
 static constexpr int kGameHeightLast  = kGameHeight - 1;
 
+// SCUMM-style verb/inventory strip (kEnhUIUX enhancement only).
+static constexpr int kUIHeight       = 64;
+static constexpr int kScreenHeight   = kGameHeight + kUIHeight;
+static constexpr int kScreenHeightLast = kScreenHeight - 1;
+
 } // namespace Macs2
 
 #endif // MACS2_CONSTANTS_H
diff --git a/engines/macs2/module.mk b/engines/macs2/module.mk
index 86740397e05..348110dc297 100644
--- a/engines/macs2/module.mk
+++ b/engines/macs2/module.mk
@@ -11,6 +11,7 @@ MODULE_OBJS = \
 	metaengine.o \
 	saveload.o \
 	scriptexecutor.o \
+	scummui.o \
 	view1.o
 
 ifdef USE_IMGUI
diff --git a/engines/macs2/saveload.cpp b/engines/macs2/saveload.cpp
index 0a41cb43f4c..b7c52640911 100644
--- a/engines/macs2/saveload.cpp
+++ b/engines/macs2/saveload.cpp
@@ -841,7 +841,9 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
 		// the per-object loop could populate their runtime walk/draw/dirty state.
 		// Do NOT recreate them here - that would discard the loaded fields.
 		view1->rebuildCharacterLookupTable();
-		view1->setInventorySource(GameObjects::instance().getProtagonistObject());
+		view1->refreshProtagonistInventoryAfterLoad(actorIndex);
+		view1->_uiPanelState = View1::kUiPanelNone;
+		view1->ensureScummVerbUI();
 
 		// Restore UseInventory cursor image after load.
 		// The cursor slot is only populated when clicking an inventory item in the panel;
@@ -865,6 +867,7 @@ Common::Error Macs2Engine::syncGame(Common::Serializer &s) {
 		} else {
 			view1->startFadingWithSpeed(8);
 		}
+		view1->presentFrame();
 	}
 
 	return Common::kNoError;
diff --git a/engines/macs2/scummui.cpp b/engines/macs2/scummui.cpp
new file mode 100644
index 00000000000..3231ad39e8c
--- /dev/null
+++ b/engines/macs2/scummui.cpp
@@ -0,0 +1,414 @@
+/* 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 "macs2/scummui.h"
+
+#include "macs2/gameobjects.h"
+#include "macs2/macs2.h"
+#include "macs2/view1.h"
+
+namespace Macs2 {
+
+static Common::String getObjectDisplayName(const GameObject *obj) {
+	if (!obj)
+		return Common::String();
+
+	const GameObjects &objects = GameObjects::instance();
+	if (obj->_index < objects._objectNames.size() && !objects._objectNames[obj->_index].empty())
+		return objects._objectNames[obj->_index];
+
+	return Common::String();
+}
+
+const ScummUI::VerbDef ScummUI::kVerbs[4] = {
+	{"Walk", Script::MouseMode::Walk},
+	{"Look", Script::MouseMode::Look},
+	{"Use", Script::MouseMode::Use},
+	{"Talk", Script::MouseMode::Talk}
+};
+
+ScummUI::ScummUI(View1 *view)
+	: _view(view), _activeVerbIndex(0), _hoveredVerb(-1), _hoveredItemIndex(-1), _hoveredScrollButton(-1),
+	  _inventoryScrollOffset(0) {
+}
+
+bool ScummUI::isPointInUI(const Common::Point &pos) const {
+	return pos.y >= kUITop;
+}
+
+void ScummUI::syncInventory() {
+	rebuildProtagonistItems();
+	const int maxOffset = MAX(0, (int)_protagonistItems.size() - kInvCols * kInvRows);
+	if (_inventoryScrollOffset > maxOffset)
+		_inventoryScrollOffset = maxOffset;
+	if (_inventoryScrollOffset < 0)
+		_inventoryScrollOffset = 0;
+}
+
+void ScummUI::rebuildProtagonistItems() {
+	_protagonistItems.clear();
+
+	if (_view->isInventorySourceProtagonist()) {
+		_protagonistItems = _view->_inventoryItems;
+		return;
+	}
+
+	const uint16 invScene = Scenes::instance()._currentActorIndex + 0x400;
+	for (GameObject *obj : GameObjects::instance()._objects) {
+		if (obj && obj->_sceneIndex == invScene)
+			_protagonistItems.push_back(obj);
+	}
+}
+
+void ScummUI::resetInventoryAfterLoad() {
+	_inventoryScrollOffset = 0;
+	_hoveredItemIndex = -1;
+	_hoveredScrollButton = -1;
+	_hoveredVerb = -1;
+	_sentenceObject.clear();
+	rebuildProtagonistItems();
+	syncActiveVerbFromCursorMode();
+
+	if (_view->_activeInventoryItem) {
+		bool inInventory = false;
+		for (GameObject *obj : _protagonistItems) {
+			if (obj == _view->_activeInventoryItem) {
+				inInventory = true;
+				break;
+			}
+		}
+		if (!inInventory) {
+			_view->_activeInventoryItem = nullptr;
+			g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
+		}
+	}
+}
+
+void ScummUI::syncActiveVerbFromCursorMode() {
+	const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
+	if (mode == Script::MouseMode::UseInventory) {
+		_activeVerbIndex = 2;
+		return;
+	}
+
+	for (int i = 0; i < 4; i++) {
+		if (kVerbs[i].mode == mode) {
+			_activeVerbIndex = i;
+			return;
+		}
+	}
+}
+
+void ScummUI::draw(Graphics::ManagedSurface &s) {
+	syncActiveVerbFromCursorMode();
+	rebuildProtagonistItems();
+	_view->drawBorderSide(Common::Point(0, kUITop), Common::Point(kScreenWidth, kUIHeight), s);
+	drawSentenceLine(s);
+	drawVerbBar(s);
+	drawInventoryStrip(s);
+}
+
+void ScummUI::drawUIButton(const Common::Rect &rect, bool pressed, Graphics::ManagedSurface &s) {
+	_view->drawBorderSide(Common::Point(rect.left, rect.top), Common::Point(rect.width(), rect.height()), s);
+	const View1::BorderStyle &style = pressed ? View1::kBorderPressed : View1::kBorderRaised;
+	_view->drawNinePatchBorder(Common::Point(rect.left, rect.top), Common::Point(rect.width(), rect.height()),
+							   style, false, false, s);
+}
+
+void ScummUI::drawSentenceLine(Graphics::ManagedSurface &s) {
+	Common::String sentence;
+	const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
+
+	if (mode == Script::MouseMode::UseInventory && _view->_activeInventoryItem) {
+		sentence = "Use";
+		const Common::String itemName = getObjectDisplayName(_view->_activeInventoryItem);
+		if (!itemName.empty())
+			sentence += " " + itemName;
+	} else if (_activeVerbIndex >= 0 && _activeVerbIndex < 4) {
+		sentence = kVerbs[_activeVerbIndex].label;
+	}
+	if (!_sentenceObject.empty()) {
+		if (!sentence.empty()) {
+			if (mode == Script::MouseMode::UseInventory)
+				sentence += " with ";
+			else
+				sentence += " ";
+		}
+		sentence += _sentenceObject;
+	}
+
+	if (sentence.empty())
+		return;
+
+	const bool usePanelFont = g_engine->numPanelGlyphs > 0;
+	const GlyphData *font = usePanelFont ? g_engine->_panelGlyphs : g_engine->_glyphs;
+	const uint16 fontCount = usePanelFont ? g_engine->numPanelGlyphs : g_engine->numGlyphs;
+	const int glyphH = usePanelFont ? g_engine->maxPanelGlyphHeight : g_engine->maxGlyphHeight;
+	if (usePanelFont)
+		sentence.toUppercase();
+	const int textY = kUITop + MAX(0, (kSentenceH - glyphH) / 2);
+	const int textX = MAX(0, (kScreenWidth - _view->measureStringWithFont(sentence, font, fontCount)) / 2);
+	_view->renderStringWithFontTo(textX, textY, sentence, font, fontCount, s);
+}
+
+void ScummUI::drawVerbBar(Graphics::ManagedSurface &s) {
+	for (int i = 0; i < 4; i++) {
+		const Common::Rect r = getVerbRect(i);
+		const bool isActive = (i == _activeVerbIndex);
+		const bool isHovered = (i == _hoveredVerb);
+
+		drawUIButton(r, isActive || isHovered, s);
+
+		const int textX = r.left + (r.width() - (int)strlen(kVerbs[i].label) * 6) / 2;
+		const int textY = r.top + (r.height() - (int)g_engine->maxGlyphHeight) / 2;
+		_view->renderStringTo(textX, textY, kVerbs[i].label, s);
+	}
+}
+
+int ScummUI::getScrollButtonWidth() const {
+	uint16 maxW = 0;
+	const Common::Array<uint16> &indices = g_engine->inventoryIconIndices;
+	for (int i = 2; i <= 3 && i < (int)indices.size(); i++) {
+		const int imgIdx = (int)indices[i] - 1;
+		if (imgIdx >= 0 && imgIdx < (int)g_engine->_imageResources.size())
+			maxW = MAX(maxW, g_engine->_imageResources[imgIdx]._width);
+	}
+	return MAX(22, (int)maxW + 6);
+}
+
+int ScummUI::getInvArrowX() const {
+	return kInvX;
+}
+
+void ScummUI::drawScrollButton(Graphics::ManagedSurface &s, const Common::Rect &rect,
+							   int iconResourceIndex, bool hovered) {
+	drawUIButton(rect, hovered, s);
+
+	if (iconResourceIndex < 0 || iconResourceIndex >= (int)g_engine->_imageResources.size())
+		return;
+
+	const AnimFrame &frame = g_engine->_imageResources[iconResourceIndex];
+	if (frame._data.empty() || frame._width == 0 || frame._height == 0)
+		return;
+
+	int iconX = rect.left + (rect.width() - frame._width) / 2;
+	int iconY = rect.top + (rect.height() - frame._height) / 2;
+	if (hovered) {
+		iconX++;
+		iconY++;
+	}
+	_view->drawSprite(iconX, iconY, frame, s, false);
+}
+
+void ScummUI::drawInventoryStrip(Graphics::ManagedSurface &s) {
+	const Common::Array<uint16> &indices = g_engine->inventoryIconIndices;
+	const int upIconIdx = (indices.size() > 2) ? (int)indices[2] - 1 : -1;
+	const int downIconIdx = (indices.size() > 3) ? (int)indices[3] - 1 : -1;
+
+	drawScrollButton(s, getInvScrollLeftRect(), upIconIdx, _hoveredScrollButton == 0);
+	drawScrollButton(s, getInvScrollRightRect(), downIconIdx, _hoveredScrollButton == 1);
+
+	const Common::Array<GameObject *> items = getProtagonistItems();
+	const int maxVisible = kInvCols * kInvRows;
+	for (int i = 0; i < maxVisible; i++) {
+		const Common::Rect r = getInvItemRect(i);
+		drawUIButton(r, true, s);
+
+		const int itemIdx = _inventoryScrollOffset + i;
+		if (itemIdx >= (int)items.size())
+			continue;
+
+		const bool isHovered = (i == _hoveredItemIndex);
+		const bool isActive = (_view->_activeInventoryItem == items[itemIdx]);
+
+		AnimFrame *icon = _view->getInventoryIcon(items[itemIdx]);
+		if (icon && !icon->_data.empty()) {
+			_view->drawSpriteFitted(r, *icon, s, kInvIconInset);
+		}
+		delete icon;
+
+		if (isActive) {
+			_view->renderStringTo(r.left + 1, r.top, "*", s);
+		} else if (isHovered) {
+			_view->renderStringTo(r.left + 1, r.top, ".", s);
+		}
+	}
+}
+
+Common::Array<GameObject *> ScummUI::getProtagonistItems() const {
+	return _protagonistItems;
+}
+
+bool ScummUI::handleClick(const Common::Point &pos, bool scriptsRunning) {
+	for (int i = 0; i < 4; i++) {
+		if (getVerbRect(i).contains(pos)) {
+			_activeVerbIndex = i;
+			g_engine->setCursorMode(kVerbs[i].mode);
+			_view->_activeInventoryItem = nullptr;
+			g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
+			_view->updateCursor();
+			return true;
+		}
+	}
+
+	if (getInvScrollLeftRect().contains(pos)) {
+		if (_inventoryScrollOffset > 0)
+			_inventoryScrollOffset -= kInvCols * kInvRows;
+		return true;
+	}
+	if (getInvScrollRightRect().contains(pos)) {
+		const int maxItems = (int)getProtagonistItems().size();
+		const int maxVisible = kInvCols * kInvRows;
+		if (_inventoryScrollOffset + maxVisible < maxItems)
+			_inventoryScrollOffset += maxVisible;
+		return true;
+	}
+
+	if (scriptsRunning)
+		return true;
+
+	const Common::Array<GameObject *> items = getProtagonistItems();
+	const int maxVisible = kInvCols * kInvRows;
+	for (int i = 0; i < maxVisible; i++) {
+		const int itemIdx = _inventoryScrollOffset + i;
+		if (itemIdx >= (int)items.size())
+			break;
+
+		if (!getInvItemRect(i).contains(pos))
+			continue;
+
+		GameObject *item = items[itemIdx];
+		const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
+
+		if (mode == Script::MouseMode::Look) {
+			g_engine->_scriptExecutor->_interactedObjectID = 0x400 + item->_index;
+			g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
+			_view->_pendingPanelRequest = View1::kPanelRequestInventory;
+			g_engine->runScriptExecutor(false);
+			_view->_pendingPanelRequest = View1::kPanelRequestNone;
+		} else if (mode == Script::MouseMode::Use) {
+			_view->_activeInventoryItem = item;
+			g_engine->_scriptExecutor->_interactedInventoryItemId = 0x400 + item->_index;
+			AnimFrame *icon = _view->getInventoryIcon(item);
+			if (icon != nullptr) {
+				const int cursorSlot = (int)Script::MouseMode::UseInventory - 1;
+				g_engine->_imageResources[cursorSlot] = *icon;
+				delete icon;
+			}
+			g_engine->setCursorMode(Script::MouseMode::UseInventory);
+			_view->updateCursor();
+		} else if (mode == Script::MouseMode::UseInventory && _view->_activeInventoryItem) {
+			g_engine->_scriptExecutor->_interactedObjectID = 0x400 + _view->_activeInventoryItem->_index;
+			g_engine->_scriptExecutor->_interactedInventoryItemId = 0x400 + item->_index;
+			_view->_pendingPanelRequest = View1::kPanelRequestInventory;
+			g_engine->runScriptExecutor(false);
+			_view->_pendingPanelRequest = View1::kPanelRequestNone;
+			_view->_activeInventoryItem = nullptr;
+			g_engine->setCursorMode(Script::MouseMode::Use);
+			_view->updateCursor();
+			syncInventory();
+		}
+		return true;
+	}
+
+	return true;
+}
+
+void ScummUI::handleMouseMove(const Common::Point &pos) {
+	const int oldHoveredVerb = _hoveredVerb;
+	const int oldHoveredItemIndex = _hoveredItemIndex;
+	const int oldHoveredScrollButton = _hoveredScrollButton;
+
+	_hoveredVerb = -1;
+	_hoveredItemIndex = -1;
+	_hoveredScrollButton = -1;
+	clearSentenceObject();
+
+	for (int i = 0; i < 4; i++) {
+		if (getVerbRect(i).contains(pos)) {
+			_hoveredVerb = i;
+			break;
+		}
+	}
+
+	if (_hoveredVerb < 0) {
+		if (getInvScrollLeftRect().contains(pos)) {
+			_hoveredScrollButton = 0;
+		} else if (getInvScrollRightRect().contains(pos)) {
+			_hoveredScrollButton = 1;
+		} else {
+			const Common::Array<GameObject *> items = getProtagonistItems();
+			const int maxVisible = kInvCols * kInvRows;
+			for (int i = 0; i < maxVisible; i++) {
+				const int itemIdx = _inventoryScrollOffset + i;
+				if (itemIdx >= (int)items.size())
+					break;
+				if (getInvItemRect(i).contains(pos)) {
+					_hoveredItemIndex = i;
+					break;
+				}
+			}
+		}
+	}
+
+	if (oldHoveredVerb != _hoveredVerb || oldHoveredItemIndex != _hoveredItemIndex ||
+		oldHoveredScrollButton != _hoveredScrollButton) {
+		_view->presentFrame();
+	}
+}
+
+void ScummUI::updateSentenceLine(const Common::String &objectName) {
+	_sentenceObject = objectName;
+}
+
+void ScummUI::clearSentenceObject() {
+	_sentenceObject.clear();
+}
+
+Common::Rect ScummUI::getVerbRect(int index) const {
+	const int col = index % kVerbCols;
+	const int row = index / kVerbCols;
+	const int x = col * kVerbW;
+	const int y = kVerbY + row * kVerbH;
+	return Common::Rect(x, y, x + kVerbW, y + kVerbH);
+}
+
+Common::Rect ScummUI::getInvItemRect(int index) const {
+	const int col = index % kInvCols;
+	const int row = index / kInvCols;
+	const int scrollW = getScrollButtonWidth();
+	const int x = getInvArrowX() + scrollW + col * kInvItemW;
+	const int y = kVerbY + row * kInvItemH;
+	return Common::Rect(x, y, x + kInvItemW, y + kInvItemH);
+}
+
+Common::Rect ScummUI::getInvScrollLeftRect() const {
+	const int scrollW = getScrollButtonWidth();
+	return Common::Rect(getInvArrowX(), kVerbY, getInvArrowX() + scrollW, kVerbY + kVerbH * kVerbRows);
+}
+
+Common::Rect ScummUI::getInvScrollRightRect() const {
+	const int scrollW = getScrollButtonWidth();
+	const int x = getInvArrowX() + scrollW + kInvCols * kInvItemW;
+	return Common::Rect(x, kVerbY, x + scrollW, kVerbY + kVerbH * kVerbRows);
+}
+
+} // namespace Macs2
diff --git a/engines/macs2/scummui.h b/engines/macs2/scummui.h
new file mode 100644
index 00000000000..694482f9212
--- /dev/null
+++ b/engines/macs2/scummui.h
@@ -0,0 +1,101 @@
+/* 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 MACS2_SCUMMUI_H
+#define MACS2_SCUMMUI_H
+
+#include "common/rect.h"
+#include "common/str.h"
+#include "graphics/managed_surface.h"
+#include "macs2/macs2_constants.h"
+#include "macs2/scriptexecutor.h"
+
+namespace Macs2 {
+
+class View1;
+class GameObject;
+
+class ScummUI {
+public:
+	ScummUI(View1 *view);
+
+	void draw(Graphics::ManagedSurface &s);
+	bool handleClick(const Common::Point &pos, bool scriptsRunning = false);
+	void handleMouseMove(const Common::Point &pos);
+	bool isPointInUI(const Common::Point &pos) const;
+	void updateSentenceLine(const Common::String &objectName);
+	void clearSentenceObject();
+	void syncInventory();
+	void syncActiveVerbFromCursorMode();
+	void resetInventoryAfterLoad();
+
+private:
+	static constexpr int kSentenceH = 14;
+	static constexpr int kUITop = kGameHeight;
+	static constexpr int kSentenceY = kGameHeight;
+	static constexpr int kVerbY = kGameHeight + kSentenceH;
+	static constexpr int kVerbW = 64;
+	static constexpr int kVerbH = 25;
+	static constexpr int kVerbCols = 2;
+	static constexpr int kVerbRows = 2;
+	static constexpr int kInvX = 128;
+	static constexpr int kInvItemW = 34;
+	static constexpr int kInvItemH = 25;
+	static constexpr int kInvIconInset = 1;
+	static constexpr int kInvCols = 4;
+	static constexpr int kInvRows = 2;
+
+	struct VerbDef {
+		const char *label;
+		Script::MouseMode mode;
+	};
+	static const VerbDef kVerbs[4];
+
+	void drawSentenceLine(Graphics::ManagedSurface &s);
+	void drawVerbBar(Graphics::ManagedSurface &s);
+	void drawInventoryStrip(Graphics::ManagedSurface &s);
+	void drawScrollButton(Graphics::ManagedSurface &s, const Common::Rect &rect, int iconResourceIndex, bool hovered);
+	void drawUIButton(const Common::Rect &rect, bool pressed, Graphics::ManagedSurface &s);
+	void rebuildProtagonistItems();
+
+	int getScrollButtonWidth() const;
+	int getInvArrowX() const;
+
+	Common::Array<GameObject *> getProtagonistItems() const;
+
+	Common::Rect getVerbRect(int index) const;
+	Common::Rect getInvItemRect(int index) const;
+	Common::Rect getInvScrollLeftRect() const;
+	Common::Rect getInvScrollRightRect() const;
+
+	View1 *_view;
+	int _activeVerbIndex;
+	int _hoveredVerb;
+	int _hoveredItemIndex;
+	int _hoveredScrollButton;
+	int _inventoryScrollOffset;
+	Common::Array<GameObject *> _protagonistItems;
+	Common::String _sentenceObject;
+};
+
+} // namespace Macs2
+
+#endif // MACS2_SCUMMUI_H
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index c40149c5c7d..7bda706c140 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -26,6 +26,7 @@
 #include "common/debug.h"
 #include "common/system.h"
 #include "engines/enhancements.h"
+#include "engines/util.h"
 #include "graphics/cursorman.h"
 #include "graphics/paletteman.h"
 #include "macs2/debugtools.h"
@@ -33,6 +34,7 @@
 #include "macs2/gameobjects.h"
 #include "macs2/macs2.h"
 #include "macs2/music.h"
+#include "macs2/scummui.h"
 
 namespace Macs2 {
 namespace {
@@ -139,9 +141,57 @@ View1::View1() : UIElement("View1") {
 	rebuildCharacterLookupTable();
 	_inventorySource = protagonist->_gameObject;
 	_inventoryButtonLocations.resize(6);
+
+	if (hasScummVerbUI()) {
+		_scummUI = new ScummUI(this);
+		_bounds = Common::Rect(0, 0, kScreenWidth, kScreenHeight);
+		_innerBounds = _bounds;
+		setInventorySource(_inventorySource);
+	}
+}
+
+void View1::ensureScummVerbUI() {
+	if (!hasScummVerbUI())
+		return;
+	if (!_scummUI) {
+		_scummUI = new ScummUI(this);
+		if (_inventorySource)
+			setInventorySource(_inventorySource);
+	}
+	if (_bounds.height() != kScreenHeight) {
+		_bounds = Common::Rect(0, 0, kScreenWidth, kScreenHeight);
+		_innerBounds = _bounds;
+		::initGraphics(kScreenWidth, kScreenHeight);
+	}
+}
+
+bool View1::hasScummVerbUI() const {
+	return g_engine->enhancementEnabled(kEnhUIUX);
+}
+
+bool View1::shouldShowScummVerbUI() const {
+	if (!hasScummVerbUI())
+		return false;
+	if (_currentMode == ViewMode::VM_HELP)
+		return false;
+	if (_isShowingTextBox || _isShowingDialoguePanel)
+		return false;
+	if (_uiPanelState == kUiPanelContainerInventory || _uiPanelState == kUiPanelSaveLoad)
+		return false;
+	if (g_engine->_scriptExecutor->_cursorMode == Script::MouseMode::Disabled)
+		return false;
+
+	// Use the actor object table directly; Character lookup can lag behind scene changes.
+	const GameObject *actor = GameObjects::getObjectByIndex(Scenes::instance()._currentActorIndex);
+	if (!actor)
+		return false;
+
+	const uint16 scene = (uint16)Scenes::instance()._currentSceneIndex;
+	return actor->_sceneIndex == scene;
 }
 
 View1::~View1() {
+	delete _scummUI;
 	for (Character *c : _characters) {
 		delete c;
 	}
@@ -182,6 +232,14 @@ void View1::openInventory(GameObject *newInventorySource) {
 
 	setInventorySource(newInventorySource);
 	_pendingPanelRequest = kPanelRequestNone; // Binary: g_wPendingPanelRequest = 0
+
+	// SCUMM verb UI: protagonist inventory is always visible in the strip.
+	if (hasScummVerbUI() && newInventorySource->_index == Scenes::instance()._currentActorIndex) {
+		if (_scummUI)
+			_scummUI->syncInventory();
+		return;
+	}
+
 	// Binary: g_wUiPanelState = 2 for protagonist, 3 for container
 	_uiPanelState = (newInventorySource->_index == Scenes::instance()._currentActorIndex)
 						? kUiPanelInventory
@@ -233,15 +291,49 @@ void View1::setInventorySource(GameObject *newInventorySource) {
 	_inventorySource = newInventorySource;
 	// Rebuild inventory list from all objects whose SceneIndex matches.
 	// Binary (syncInventoryObjectList at 1008:071e) checks: object.sceneIndex == actorIndex + 0x400.
-	// The +0x400 offset encodes "inside this container/actor's inventory".
+	const uint16 inventorySceneId = (_inventorySource->_index == Scenes::instance()._currentActorIndex)
+										? (uint16)(Scenes::instance()._currentActorIndex + 0x400)
+										: (uint16)(_inventorySource->_index + 0x400);
 	_inventoryItems.clear();
 
-	const uint16 inventorySceneId = _inventorySource->_index + 0x400;
 	for (GameObject *currentObject : GameObjects::instance()._objects) {
 		if (currentObject != nullptr && currentObject->_sceneIndex == inventorySceneId) {
 			_inventoryItems.push_back(currentObject);
 		}
 	}
+	if (hasScummVerbUI() && _scummUI)
+		_scummUI->syncInventory();
+}
+
+void View1::refreshProtagonistInventoryAfterLoad(uint16 actorIndex) {
+	_inventorySource = GameObjects::instance().getProtagonistObject();
+	const uint16 invScene = actorIndex + 0x400;
+
+	Common::Array<GameObject *> validated;
+	for (GameObject *obj : _inventoryItems) {
+		if (obj && obj->_sceneIndex == invScene)
+			validated.push_back(obj);
+	}
+
+	for (GameObject *obj : GameObjects::instance()._objects) {
+		if (!obj || obj->_sceneIndex != invScene)
+			continue;
+
+		bool found = false;
+		for (GameObject *listed : validated) {
+			if (listed == obj) {
+				found = true;
+				break;
+			}
+		}
+		if (!found)
+			validated.push_back(obj);
+	}
+
+	_inventoryItems = validated;
+
+	if (hasScummVerbUI() && _scummUI)
+		_scummUI->resetInventoryAfterLoad();
 }
 
 bool View1::isInventorySourceProtagonist() const {
@@ -252,6 +344,8 @@ void View1::transferInventoryItem(GameObject *item, GameObject *targetContainer)
 	int index = findInventoryItem(item);
 	_inventoryItems.remove_at(index);
 	item->_sceneIndex = targetContainer->_index + 0x400;
+	if (hasScummVerbUI() && _scummUI)
+		_scummUI->syncInventory();
 }
 
 int View1::findInventoryItem(const GameObject *item) {
@@ -333,6 +427,22 @@ void View1::updateCursor(const byte *palette) {
 	// Original indexes cursor array as: base + mode * 16 - 16, i.e. 0-based index = mode - 1.
 	// The array has 33 entries (indices 0-32). Cursor modes 0x13-0x1A map to entries 18-25.
 	int mode = (int)g_engine->_scriptExecutor->_cursorMode - 1;
+
+	// SCUMM-style UI: gameplay verbs share the walk cursor; the sentence line shows the active verb.
+	if (hasScummVerbUI()) {
+		const Script::MouseMode cursorMode = g_engine->_scriptExecutor->_cursorMode;
+		switch (cursorMode) {
+		case Script::MouseMode::Talk:
+		case Script::MouseMode::Look:
+		case Script::MouseMode::Use:
+		case Script::MouseMode::Walk:
+			mode = (int)Script::MouseMode::Walk - 1;
+			break;
+		default:
+			break;
+		}
+	}
+
 	if (mode < 0 || mode >= kNumLoadedCursors) {
 		warning("Invalid cursor mode %d, falling back to Walk cursor", mode);
 		mode = (int)Script::MouseMode::Walk - 1;
@@ -714,6 +824,9 @@ void View1::layoutActionBarButtons() {
 }
 
 void View1::openMainMenu(Common::Point clickedPosition) {
+	if (hasScummVerbUI())
+		return;
+
 	// Binary handleInput: save cursor and set to PanelCursor (0x19)
 	_savedCursorMode = g_engine->_scriptExecutor->_cursorMode;
 	g_engine->setCursorMode(Script::MouseMode::PanelCursor);
@@ -934,6 +1047,9 @@ void View1::transferPickupTarget(GameObject *targetObject) {
 		}
 	}
 
+	if (hasScummVerbUI() && _scummUI)
+		_scummUI->syncInventory();
+
 	// Binary sets g_wNeedsRedraw and restores scene background over the panel area.
 	redraw();
 }
@@ -1082,6 +1198,7 @@ void View1::startFadingWithSpeed(uint16 speed) {
 	_fadeMode = FadeMode::None;
 	_paletteDirty = false;
 	endFadeCursorSuppression(g_engine->_pal);
+	redraw();
 }
 
 void View1::beginFadeCursorSuppression() {
@@ -1325,6 +1442,8 @@ bool View1::handleContainerInventoryClick(const MouseDownMessage &msg) {
 				updateCursor();
 				g_engine->_scriptExecutor->_inventoryActionFlag = true;
 				setInventorySource(_inventorySource);
+				if (hasScummVerbUI() && _scummUI)
+					_scummUI->syncInventory();
 			}
 			break;
 		}
@@ -1364,6 +1483,8 @@ bool View1::handleContainerInventoryClick(const MouseDownMessage &msg) {
 		}
 		g_engine->setCursorMode(Script::MouseMode::UseInventory);
 		updateCursor();
+		if (hasScummVerbUI() && _scummUI)
+			_scummUI->syncInventory();
 		return true;
 	}
 
@@ -1490,6 +1611,14 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 			return handleHelpClick(msg);
 		}
 
+		if (shouldShowScummVerbUI() && _scummUI && _scummUI->isPointInUI(msg._pos)) {
+			if (g_engine->_scriptExecutor->_cursorMode != Script::MouseMode::Disabled) {
+				_scummUI->handleClick(msg._pos, g_engine->_scriptExecutor->isExecuting());
+				presentFrame();
+			}
+			return true;
+		}
+
 		// Handle original save/load panel clicks
 		if (_uiPanelState == kUiPanelSaveLoad) {
 			handleOriginalSaveLoadClick(msg._pos);
@@ -1565,6 +1694,9 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 		}
 
 		if (g_engine->_scriptExecutor->_cursorMode == Script::MouseMode::Walk) {
+			if (shouldShowScummVerbUI() && msg._pos.y >= kGameHeight)
+				return true;
+
 			Character *protagonist = getCharacterByIndex(Scenes::instance()._currentActorIndex);
 			if (protagonist == nullptr) {
 				debugC(kDebugScript, "Ignoring walk click without active actor character in the scene");
@@ -1605,6 +1737,9 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 		}
 
 		// Check if we hit something
+		if (shouldShowScummVerbUI() && msg._pos.y >= kGameHeight)
+			return true;
+
 		// Original order: getHotspotAtPoint first, then drawCharactersAndHitTest overrides.
 		// Our order (objects first, fallback to background) produces the same result.
 		uint16 index = getHitObjectID(Common::Point(msg._pos.x, msg._pos.y));
@@ -1677,10 +1812,22 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 		}
 
 		// From handleInput (1008:e8bf): right-click when not executing and cursor != Disabled
-		// opens the action bar at the mouse position.
+		// opens the action bar at the mouse position (or cycles verbs with SCUMM UI).
 		if (g_engine->_scriptExecutor->_cursorMode == Script::MouseMode::Disabled) {
 			return true;
 		}
+		if (hasScummVerbUI()) {
+			if (shouldShowScummVerbUI()) {
+				g_engine->nextCursorMode();
+				_activeInventoryItem = nullptr;
+				g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
+				if (_scummUI)
+					_scummUI->syncActiveVerbFromCursorMode();
+				updateCursor();
+				presentFrame();
+			}
+			return true;
+		}
 		if (_uiPanelState != kUiPanelActionBar) {
 			openMainMenu(msg._pos);
 		} else {
@@ -1817,6 +1964,26 @@ bool View1::msgMouseUp(const MouseUpMessage &msg) {
 bool View1::msgMouseMove(const MouseMoveMessage &msg) {
 	_hoverAreaId = g_engine->_scriptExecutor->getAreaAtPoint(msg._pos.x, msg._pos.y);
 	_hoverHotspotId = g_engine->getHotspotAtPoint(msg._pos);
+
+	if (shouldShowScummVerbUI() && _scummUI) {
+		if (_scummUI->isPointInUI(msg._pos)) {
+			_scummUI->handleMouseMove(msg._pos);
+		} else if (msg._pos.y < kGameHeight) {
+			_scummUI->clearSentenceObject();
+			uint16 index = getHitObjectID(msg._pos);
+			if (index == 0)
+				index = g_engine->getHotspotAtPoint(msg._pos);
+			if (index != 0 && index >= 0x400) {
+				const uint16 objIndex = index - 0x400;
+				if (objIndex < GameObjects::instance()._objectNames.size()) {
+					const Common::String &name = GameObjects::instance()._objectNames[objIndex];
+					if (!name.empty())
+						_scummUI->updateSentenceLine(name);
+				}
+			}
+		}
+	}
+
 	return true;
 }
 
@@ -1889,14 +2056,29 @@ bool View1::msgKeypress(const KeypressMessage &msg) {
 	// Binary (handleInput 1008:edff): UI panels only open when not executing and cursor != Disabled.
 	if (!g_engine->_scriptExecutor->isExecuting() && g_engine->_scriptExecutor->_cursorMode != Script::MouseMode::Disabled) {
 		if (msg.ascii == (uint16)'i') {
-			if (_uiPanelState != kUiPanelInventory) {
+			if (hasScummVerbUI()) {
+				if (_uiPanelState == kUiPanelContainerInventory)
+					closeInventory();
+			} else if (_uiPanelState != kUiPanelInventory) {
 				openInventory(GameObjects::instance().getProtagonistObject());
 			} else {
 				closeInventory();
 			}
 		} else if (msg.ascii == 'n') {
-			Common::Point mousePos = g_system->getEventManager()->getMousePos();
-			openMainMenu(mousePos);
+			if (hasScummVerbUI()) {
+				if (shouldShowScummVerbUI()) {
+					g_engine->nextCursorMode();
+					_activeInventoryItem = nullptr;
+					g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
+					if (_scummUI)
+						_scummUI->syncActiveVerbFromCursorMode();
+					updateCursor();
+					presentFrame();
+				}
+			} else {
+				Common::Point mousePos = g_system->getEventManager()->getMousePos();
+				openMainMenu(mousePos);
+			}
 		}
 	}
 
@@ -1978,7 +2160,8 @@ void View1::draw() {
 
 	// We keep the inventory on but don't draw it in case we display a string
 	// i.e. a description of an item
-	if ((_uiPanelState == kUiPanelInventory || _uiPanelState == kUiPanelContainerInventory) && !_isShowingTextBox && !_isShowingDialoguePanel) {
+	const bool showProtagonistInventory = _uiPanelState == kUiPanelInventory && !hasScummVerbUI();
+	if ((showProtagonistInventory || _uiPanelState == kUiPanelContainerInventory) && !_isShowingTextBox && !_isShowingDialoguePanel) {
 		drawInventory(s);
 	}
 
@@ -2037,6 +2220,18 @@ void View1::draw() {
 			}
 		}
 	}
+
+	if (hasScummVerbUI()) {
+		ensureScummVerbUI();
+		if (_scummUI && !_isShowingTextBox && !_isShowingDialoguePanel) {
+			Graphics::ManagedSurface fullScreen(*g_events->getScreen(), Common::Rect(0, 0, kScreenWidth, kScreenHeight));
+			if (shouldShowScummVerbUI()) {
+				_scummUI->draw(fullScreen);
+			} else {
+				fullScreen.fillRect(Common::Rect(0, kGameHeight, kScreenWidth, kScreenHeight), 0);
+			}
+		}
+	}
 }
 
 void View1::drawSceneUpdate() {
@@ -2447,6 +2642,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 			const byte *pixelData = frame._data.data();
 
 			// drawAllCharacters @ 1008:9573-9754: drawAnimFrame / drawAnimFrameShaded / drawAnimFrameDepth
+			const bool clipGameArea = hasScummVerbUI();
 			if (obj->_hasScaling) {
 				drawSpriteTransparent(shadingTableOffset, depthThreshold, scalingFactor,
 									  drawX, drawY, frame._width, frame._height, pixelData, *surface);
@@ -2455,7 +2651,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 								 frame._width, frame._height, pixelData, *surface);
 			} else {
 				drawSprite(drawX, drawY, frame._width, frame._height,
-						   const_cast<byte *>(pixelData), *surface, false);
+						   const_cast<byte *>(pixelData), *surface, false, false, 0, clipGameArea);
 			}
 
 			// drawAllCharacters @ 1008:9759: wLastDrawX/Y exclude per-frame offsetX/offsetY
@@ -2583,6 +2779,9 @@ void View1::drawInventory(Graphics::ManagedSurface &s) {
 
 	// Draw the buttons at the bottom
 	for (int i = 0; i < 6; i++) {
+		if (hasScummVerbUI() && !isInventorySourceProtagonist() && i == (int)InventoryButtonIndex::Drop)
+			continue;
+
 		uint16 index = iconIndices[i];
 		AnimFrame &currentFrame = g_engine->_imageResources[index - 1];
 		drawNinePatchBorder(Common::Point(buttonX, buttonY), Common::Point(buttonW, buttonH), kBorderRaised, false, false, s);
@@ -2667,7 +2866,7 @@ GameObject *View1::getClickedInventoryItem(const Common::Point &p) {
 	return nullptr;
 }
 
-void View1::drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data, Graphics::ManagedSurface &s, bool mirrored, bool useDepth, uint8 depth) {
+void View1::drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data, Graphics::ManagedSurface &s, bool mirrored, bool useDepth, uint8 depth, bool clipToGameArea) {
 	for (int currentX = 0; currentX < width; currentX++) {
 		int actualX = mirrored ? width - currentX - 1 : currentX;
 		for (int currentY = 0; currentY < height; currentY++) {
@@ -2676,6 +2875,8 @@ void View1::drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data
 				int finalX = x + actualX;
 				int finalY = y + currentY;
 				if (finalX >= 0 && finalX < s.w && finalY >= 0 && finalY < s.h) {
+					if (clipToGameArea && finalY >= kGameHeight)
+						continue;
 					// Check for depth
 					uint8 bgDepth = g_engine->_depthMap.getPixel(finalX, finalY);
 					// Depth test: draw pixel only if depth map value < character depth
@@ -2689,12 +2890,12 @@ void View1::drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data
 	}
 }
 
-void View1::drawSprite(const Common::Point &pos, uint16 width, uint16 height, byte *data, Graphics::ManagedSurface &s, bool mirrored, bool useDepth, uint8 depth) {
-	drawSprite(pos.x, pos.y, width, height, data, s, mirrored, useDepth, depth);
+void View1::drawSprite(const Common::Point &pos, uint16 width, uint16 height, byte *data, Graphics::ManagedSurface &s, bool mirrored, bool useDepth, uint8 depth, bool clipToGameArea) {
+	drawSprite(pos.x, pos.y, width, height, data, s, mirrored, useDepth, depth, clipToGameArea);
 }
 
-void View1::drawSprite(int16 x, int16 y, const Sprite &sprite, Graphics::ManagedSurface &s, bool mirrored, bool useDepth, uint8 depth) {
-	drawSprite(x, y, sprite._width, sprite._height, const_cast<byte *>(sprite._data.data()), s, mirrored, useDepth, depth);
+void View1::drawSprite(int16 x, int16 y, const Sprite &sprite, Graphics::ManagedSurface &s, bool mirrored, bool useDepth, uint8 depth, bool clipToGameArea) {
+	drawSprite(x, y, sprite._width, sprite._height, const_cast<byte *>(sprite._data.data()), s, mirrored, useDepth, depth, clipToGameArea);
 }
 
 void View1::drawSpriteClipped(uint16 x, uint16 y, Common::Rect &clippingRect, uint16 width, uint16 height, const byte *const data, Graphics::ManagedSurface &s) {
@@ -2702,10 +2903,10 @@ void View1::drawSpriteClipped(uint16 x, uint16 y, Common::Rect &clippingRect, ui
 		for (int currentY = 0; currentY < height; currentY++) {
 			uint8 val = data[currentY * width + currentX];
 			if (val != 0) {
-				if (clippingRect.contains(x + currentX, y + currentY)) {
-					if (x + currentX < kScreenWidth && y + currentY < kGameHeight)
-						s.setPixel(x + currentX, y + currentY, val);
-				}
+				const int px = x + currentX;
+				const int py = y + currentY;
+				if (clippingRect.contains(px, py) && px >= 0 && px < s.w && py >= 0 && py < s.h)
+					s.setPixel(px, py, val);
 			}
 		}
 	}
@@ -2774,7 +2975,7 @@ void View1::drawSpriteScaled(int shadingTableOffset, uint8 depthThreshold, int16
 	int srcRow = 0;
 	int remainingRows = srcHeight;
 	while (remainingRows > 0) {
-		if (screenY >= 0 && screenY < s.h) {
+		if (screenY >= 0 && screenY < s.h && !(hasScummVerbUI() && screenY >= kGameHeight)) {
 			int screenX = drawX;
 			for (uint16 srcX = 0; srcX < srcWidth; srcX++) {
 				if (screenX >= 0 && screenX < s.w) {
@@ -2804,7 +3005,7 @@ void View1::drawSpriteTransparent(int shadingTableOffset, uint8 depthThreshold,
 	uint16 yScaleAccum = 0;
 
 	while (remainingRows > 0) {
-		if (screenY >= 0 && screenY < s.h) {
+		if (screenY >= 0 && screenY < s.h && !(hasScummVerbUI() && screenY >= kGameHeight)) {
 			int screenX = drawX;
 			const byte *srcPtr = srcPixels + srcRowOffset;
 			int remainingSrcPixels = (int)srcWidth;
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index 41538be1134..2d3bd466e9a 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -27,6 +27,7 @@
 
 namespace Macs2 {
 
+class ScummUI;
 class GameObject;
 
 enum class ViewMode {
@@ -187,6 +188,8 @@ struct ScalingValues {
 };
 
 class View1 : public UIElement {
+	friend class ScummUI;
+
 private:
 	// drawSpriteTransparent @ 1010:0ed1 (drawAnimFrameDepth @ 1010:172c)
 	void drawSpriteTransparent(int shadingTableOffset, uint8 depthThreshold, uint16 scalingFactor,
@@ -200,6 +203,8 @@ private:
 	// Set by action bar map button on press; enterMapMode() runs on panel release.
 	bool _pendingMapOpen = false;
 
+	ScummUI *_scummUI = nullptr;
+
 	// Saved scene visuals for help screen restore (avoids changeScene on exit)
 	byte _savedPalVanilla[256 * 3] = {0};
 	Graphics::ManagedSurface _savedDepthMap;
@@ -319,9 +324,9 @@ private:
 	Common::Array<Common::Rect> _inventoryButtonLocations;
 	uint16 _inventoryScrollOffset = 0;
 
-	void drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data, Graphics::ManagedSurface &s, bool mirrored, bool useDepth = false, uint8 depth = 0);
-	void drawSprite(int16 x, int16 y, const Sprite &sprite, Graphics::ManagedSurface &s, bool mirrored, bool useDepth = false, uint8 depth = 0);
-	void drawSprite(const Common::Point &pos, uint16 width, uint16 height, byte *data, Graphics::ManagedSurface &s, bool mirrored, bool useDepth = false, uint8 depth = 0);
+	void drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data, Graphics::ManagedSurface &s, bool mirrored, bool useDepth = false, uint8 depth = 0, bool clipToGameArea = false);
+	void drawSprite(int16 x, int16 y, const Sprite &sprite, Graphics::ManagedSurface &s, bool mirrored, bool useDepth = false, uint8 depth = 0, bool clipToGameArea = false);
+	void drawSprite(const Common::Point &pos, uint16 width, uint16 height, byte *data, Graphics::ManagedSurface &s, bool mirrored, bool useDepth = false, uint8 depth = 0, bool clipToGameArea = false);
 
 	void drawSpriteClipped(uint16 x, uint16 y, Common::Rect &clippingRect, uint16 width, uint16 height, const byte *const data, Graphics::ManagedSurface &s);
 	void drawSpriteClipped(uint16 x, uint16 y, Common::Rect &clippingRect, const Sprite &sprite, Graphics::ManagedSurface &s);
@@ -348,6 +353,10 @@ public:
 	View1();
 	virtual ~View1();
 
+	bool hasScummVerbUI() const;
+	bool shouldShowScummVerbUI() const;
+	void ensureScummVerbUI();
+
 	// g_wHelpButtonDisabled (1020:23B4): when non-zero, help/map button is disabled
 	// and script scene changes use applyScenePaletteEffect instead of palette fades.
 	// Map overlay mode is scene+0x61db (_currentMode == VM_HELP), not this flag.
@@ -477,6 +486,7 @@ public:
 
 	// Sets the source for the to-be-opened inventory and updats the array of inventory objects
 	void setInventorySource(GameObject *newInventorySource);
+	void refreshProtagonistInventoryAfterLoad(uint16 actorIndex);
 	void openInventory(GameObject *newInventorySource);
 	// Binary handleInput (1008:e8bf) close path for the inventory panels.
 	void closeInventory();




More information about the Scummvm-git-logs mailing list