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

mgerhardy noreply at scummvm.org
Wed Sep 2 20:01:36 UTC 2026


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

Summary:
85451ad75d MACS2: removed getObjectHotspotName wrapper
ed2c220f07 MACS2: cleanup position check + const
4710184096 MACS2: unified names
5476e7d881 MACS2: replaced magic numbers and removed comments
5ba656ca9e MACS2: const + reduced code duplication
e8c46eb17f MACS2: extract character class into own file
fcec8c98bf MACS2: removed Button class


Commit: 85451ad75dec7a6654835b4a791ab6172cc603a2
    https://github.com/scummvm/scummvm/commit/85451ad75dec7a6654835b4a791ab6172cc603a2
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-09-02T22:01:21+02:00

Commit Message:
MACS2: removed getObjectHotspotName wrapper

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


diff --git a/engines/macs2/actionbar.cpp b/engines/macs2/actionbar.cpp
index f5975cfcc01..409ef3e0904 100644
--- a/engines/macs2/actionbar.cpp
+++ b/engines/macs2/actionbar.cpp
@@ -28,6 +28,7 @@
 #include "gui/message.h"
 #include "macs2/detection.h"
 #include "macs2/gameobjects.h"
+#include "macs2/hotspot_names.h"
 #include "macs2/macs2.h"
 #include "macs2/music.h"
 #include "macs2/view1.h"
@@ -445,7 +446,7 @@ void ActionBar::handleMouseMoveScumm(const Common::Point &pos) {
 					break;
 				if (getInvItemRect(i).contains(pos)) {
 					_hoveredItemIndex = i;
-					updateSentenceLine(getObjectHotspotName(items[itemIdx]->_index));
+					updateSentenceLine(lookupObjectHotspotName(items[itemIdx]->_index));
 					break;
 				}
 			}
@@ -541,7 +542,7 @@ Common::String ActionBar::currentTargetDisplayName() const {
 		_view->_uiPanelState == View1::kUiPanelInventory) {
 		GameObject *hovered = _view->getClickedInventoryItem(mouse);
 		if (hovered != nullptr)
-			return getObjectHotspotName(hovered->_index);
+			return lookupObjectHotspotName(hovered->_index);
 		return Common::String();
 	}
 
@@ -560,7 +561,7 @@ Common::String ActionBar::buildSentenceLine() const {
 	const Common::String targetName = currentTargetDisplayName();
 	Common::String itemName;
 	if (_view->_activeInventoryItem != nullptr) {
-		itemName = getObjectHotspotName(_view->_activeInventoryItem->_index);
+		itemName = lookupObjectHotspotName(_view->_activeInventoryItem->_index);
 	}
 
 	if (mode == Script::MouseMode::UseInventory && !itemName.empty()) {
diff --git a/engines/macs2/debugtools.cpp b/engines/macs2/debugtools.cpp
index f750ec2c8b7..599b5dd6af0 100644
--- a/engines/macs2/debugtools.cpp
+++ b/engines/macs2/debugtools.cpp
@@ -29,6 +29,7 @@
 #include "common/util.h"
 #include "macs2/detection.h"
 #include "macs2/gameobjects.h"
+#include "macs2/hotspot_names.h"
 #include "macs2/macs2.h"
 #include "macs2/music.h"
 #include "macs2/view1.h"
@@ -1199,7 +1200,7 @@ static void showInventoryWindow() {
 			if (ImGui::CollapsingHeader("Current Inventory", ImGuiTreeNodeFlags_DefaultOpen)) {
 				for (uint i = 0; i < view->_inventoryItems.size(); i++) {
 					GameObject *obj = view->_inventoryItems[i];
-					Common::String name = getObjectHotspotName(obj->_index);
+					Common::String name = lookupObjectHotspotName(obj->_index);
 					if (name.empty())
 						name = "???";
 					Common::String utf8Name = Common::U32String(name.c_str(), Common::kDos850).encode(Common::kUtf8);
@@ -1223,7 +1224,7 @@ static void showInventoryWindow() {
 						continue;
 					if (obj->_blobs.size() <= 0x13 || obj->_blobs[0x13].empty())
 						continue;
-					Common::String name = getObjectHotspotName(obj->_index);
+					Common::String name = lookupObjectHotspotName(obj->_index);
 					if (name.empty())
 						name = "???";
 					Common::String utf8Name = Common::U32String(name.c_str(), Common::kDos850).encode(Common::kUtf8);
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 5134fb45074..e85135528eb 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -2624,16 +2624,12 @@ uint16 Macs2Engine::getHotspotAtPoint(const Common::Point &p) const {
 	return 0;
 }
 
-Common::String getObjectHotspotName(uint16 objectIndex) {
-	return lookupObjectHotspotName(objectIndex);
-}
-
 Common::String lookupInteractionDisplayName(uint16 interactionId) {
 	if (interactionId >= 0x800) {
 		return lookupSceneHotspotName((uint16)Scenes::instance()._currentSceneIndex, (uint16)(interactionId - 0x800));
 	}
 	if (interactionId >= 0x400) {
-		return getObjectHotspotName((uint16)(interactionId - 0x400));
+		return lookupObjectHotspotName((uint16)(interactionId - 0x400));
 	}
 	return Common::String();
 }
@@ -2794,7 +2790,7 @@ void Macs2Engine::getHotspotPositions(Common::Array<Graphics::HotspotInfo> &hots
 			hotspotType = Graphics::kHotspotNPC;
 		}
 
-		const Common::String &name = getObjectHotspotName(entry.index);
+		const Common::String &name = lookupObjectHotspotName(entry.index);
 		hotspots.emplace_back(Graphics::HotspotInfo(screenPos, hotspotLabelToU32(name), hotspotType));
 	}
 }
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index c365b644b9b..595338aa796 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -964,7 +964,6 @@ public:
 };
 
 extern Macs2Engine *g_engine;
-Common::String getObjectHotspotName(uint16 objectIndex);
 /** Display name for a hit id: 0x400+object or 0x800+scene hotspot. */
 Common::String lookupInteractionDisplayName(uint16 interactionId);
 
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 46b7fd4c09e..15756cac99f 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -35,6 +35,7 @@
 #include "macs2/detection.h"
 #include "macs2/gameobjects.h"
 #include "macs2/amiga_decode.h"
+#include "macs2/hotspot_names.h"
 #include "macs2/macs2.h"
 #include "macs2/music.h"
 #include "macs2/actionbar.h"
@@ -2155,7 +2156,7 @@ bool View1::msgMouseMove(const MouseMoveMessage &msg) {
 			_actionBar->clearSentenceObject();
 			GameObject *hovered = getClickedInventoryItem(msg._pos);
 			if (hovered != nullptr) {
-				const Common::String &name = getObjectHotspotName(hovered->_index);
+				const Common::String &name = lookupObjectHotspotName(hovered->_index);
 				if (!name.empty())
 					_actionBar->updateSentenceLine(name);
 			}
@@ -2383,9 +2384,9 @@ void View1::draw() {
 			}
 		}
 
-		GameObject *hoveredObject = getClickedInventoryItem(mousePos);
+		const GameObject *hoveredObject = getClickedInventoryItem(mousePos);
 		if (hoveredObject != nullptr) {
-			Common::String name = getObjectHotspotName(hoveredObject->_index);
+			const Common::String &name = lookupObjectHotspotName(hoveredObject->_index);
 			if (!name.empty()) {
 				renderString(mousePos.x + 20, mousePos.y + 20, name);
 			} else {


Commit: ed2c220f07aa5bb203247125f3724a413b380384
    https://github.com/scummvm/scummvm/commit/ed2c220f07aa5bb203247125f3724a413b380384
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-09-02T22:01:21+02:00

Commit Message:
MACS2: cleanup position check + const

Changed paths:
    engines/macs2/view1.cpp


diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 15756cac99f..843e5a7b70c 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -2594,8 +2594,7 @@ bool View1::tick() {
 						// Binary gameTick (1008:e752): polls object table pos vs runtime finalDest;
 						// no on-screen Character is required (e.g. after moveObject to another scene).
 						const GameObject::StoredWalkRuntime &rt = walkObject->_storedWalkRuntime;
-						if (walkObject->_position.x == rt.pathFinalDestination.x &&
-							walkObject->_position.y == rt.pathFinalDestination.y) {
+						if (walkObject->_position == rt.pathFinalDestination) {
 							if ((int16)rt.motionTargetVerticalOffset < 0 ||
 								rt.motionTargetVerticalOffset == walkObject->_verticalOffsetScale) {
 								walkComplete = true;
@@ -2633,7 +2632,7 @@ bool View1::tick() {
 				}
 			} else if (executor->_waitForAdlibReady) {
 				drawSceneUpdate();
-				Music *music = g_engine->getMusic();
+				const Music *music = g_engine->getMusic();
 				const bool ready = music->isMidiFilePlaying() ? false : music->isPlaybackReady();
 				if (ready) {
 					executor->_waitForAdlibReady = false;


Commit: 4710184096c8892cd36ca7f42fd283d9a6511125
    https://github.com/scummvm/scummvm/commit/4710184096c8892cd36ca7f42fd283d9a6511125
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-09-02T22:01:21+02:00

Commit Message:
MACS2: unified names

Changed paths:
    engines/macs2/view1.cpp


diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 843e5a7b70c..629c4e5141e 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -1206,8 +1206,8 @@ void View1::transferPickupTarget(GameObject *targetObject) {
 		return;
 	}
 
-	Script::ScriptExecutor *executor = g_engine->_scriptExecutor;
-	const uint16 actorIndex = executor->_pickupActorObjectID;
+	Script::ScriptExecutor *exec = g_engine->_scriptExecutor;
+	const uint16 actorIndex = exec->_pickupActorObjectID;
 	if (actorIndex == 0) {
 		return;
 	}
@@ -1222,7 +1222,7 @@ void View1::transferPickupTarget(GameObject *targetObject) {
 
 	Character *itemCharacter = getCharacterByIndex(targetObject->_index);
 	if (itemCharacter != nullptr) {
-		executor->saveWalkRuntime(itemCharacter, targetObject);
+		exec->saveWalkRuntime(itemCharacter, targetObject);
 		const int index = getCharacterArrayIndex(itemCharacter);
 		if (index >= 0) {
 			itemCharacter->_markedForDeletion = true;
@@ -1255,7 +1255,7 @@ void View1::transferPickupTarget(GameObject *targetObject) {
 
 	if (_activeInventoryItem != nullptr && _activeInventoryItem->_index == targetObject->_index) {
 		_activeInventoryItem = nullptr;
-		if (executor->_cursorMode == Script::MouseMode::UseInventory) {
+		if (exec->_cursorMode == Script::MouseMode::UseInventory) {
 			g_engine->setCursorMode(Script::MouseMode::Use);
 			updateCursor();
 		}
@@ -1843,7 +1843,7 @@ void View1::walkToScreenPosition(const Common::Point &pos) {
 }
 
 bool View1::handleInput(const MouseDownMessage &msg) {
-	Script::ScriptExecutor *script = g_engine->_scriptExecutor;
+	Script::ScriptExecutor *exec = g_engine->_scriptExecutor;
 	if (msg._button == MouseMessage::MB_LEFT) {
 		// Help mode (depth-based scene preview) from handleInput (1008:e8bf).
 		// When currentMode == VM_HELP, clicking on the depth map previews scenes.
@@ -1852,8 +1852,8 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 		}
 
 		if (shouldShowActionBar() && _actionBar && _actionBar->isPointInUI(msg._pos)) {
-			if (script->_cursorMode != Script::MouseMode::Disabled) {
-				_actionBar->handleClick(msg._pos, script->isExecuting());
+			if (exec->_cursorMode != Script::MouseMode::Disabled) {
+				_actionBar->handleClick(msg._pos, exec->isExecuting());
 				presentFrame();
 			}
 			return true;
@@ -1869,11 +1869,11 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 		// text-box-dismiss gate before the interaction check. The text box (if any)
 		// is cleared as a side-effect of the script rerunning. Clear it here so the
 		// UI updates immediately, but do NOT consume the click.
-		if (_isShowingTextBox && !script->isExecuting()) {
+		if (_isShowingTextBox && !exec->isExecuting()) {
 			handleTextBoxInput();
 		}
 
-		if (_uiPanelState == kUiPanelInventory && !script->isExecuting()) {
+		if (_uiPanelState == kUiPanelInventory && !exec->isExecuting()) {
 			return handleInventoryClick(msg);
 		}
 
@@ -1881,7 +1881,7 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 			return handleContainerInventoryClick(msg);
 		}
 
-		if (_uiPanelState == kUiPanelActionBar && !script->isExecuting()) {
+		if (_uiPanelState == kUiPanelActionBar && !exec->isExecuting()) {
 			return handleActionBarClick(msg);
 		}
 
@@ -1889,7 +1889,7 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 		// From handleInput (1008:f1d4): clicks during script execution are ONLY processed
 		// if cursor is not Disabled (0x1A). When cursor is Disabled (walk/wait in progress),
 		// clicks are completely ignored.
-		if (script->isScriptMidExecution() && script->_cursorMode != Script::MouseMode::Disabled) {
+		if (exec->isScriptMidExecution() && exec->_cursorMode != Script::MouseMode::Disabled) {
 			// Binary handleInput (1008:f1d4-f225): exact sequence of unconditional checks
 			// 1. if g_wIsShowingTextBox != 0: handleTextBoxInput()
 			// 2. if g_wIsShowingDialoguePanel != 0: dismissDialoguePanel()
@@ -1916,23 +1916,23 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 				}
 			}
 			if (!_isDialogueChoiceInputActive) {
-				script->_scriptClickFlag = 0;
-				script->_scriptClickX = (uint16)msg._pos.x;
-				script->_scriptClickY = (uint16)msg._pos.y;
-				script->_scriptClickResult = 1;
+				exec->_scriptClickFlag = 0;
+				exec->_scriptClickX = (uint16)msg._pos.x;
+				exec->_scriptClickY = (uint16)msg._pos.y;
+				exec->_scriptClickResult = 1;
 				g_engine->runScriptExecutor();
 			}
 			return true;
 		}
 
-		if (script->isExecuting()) {
+		if (exec->isExecuting()) {
 			return true;
 		}
 
 		if (shouldShowActionBar() && msg._pos.y >= actionBarTopY())
 			return true;
 
-		const Script::MouseMode mode = script->_cursorMode;
+		const Script::MouseMode mode = exec->_cursorMode;
 
 		// Walk never hit-tests; other verbs interact when a target is under the cursor.
 		// Empty-ground clicks walk so the persistent verb bar does not trap the player
@@ -1954,13 +1954,13 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 				}
 
 				if (mode != Script::MouseMode::UseInventory) {
-					script->_interactedInventoryItemId = 0;
+					exec->_interactedInventoryItemId = 0;
 					_activeInventoryItem = nullptr;
 				}
 
-				script->_interactedObjectID = index;
+				exec->_interactedObjectID = index;
 				g_engine->runScriptExecutor(false);
-				script->_interactedObjectID = 0;
+				exec->_interactedObjectID = 0;
 				return true;
 			}
 		}
@@ -1976,23 +1976,23 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 			return true;
 		}
 		// Handle no other interactions during a script
-		if (script->isExecuting()) {
+		if (exec->isExecuting()) {
 			if (!_isShowingDialoguePanel && !_isDialogueChoiceInputActive &&
 				!_isShowingTextBox &&
-				!script->_overlayTextStageActive &&
-				!script->_waitForPcmSound &&
-				!script->_waitForMusicControl &&
-				!script->_waitForAdlibReady &&
-				!script->_waitForObjectAnimStep &&
-				!script->_waitForSpecialAnimStep &&
-				!script->_waitForDeltaAnim &&
-				!script->_waitForDeltaSpeed &&
-				script->canOpenSaveMenu()) {
+				!exec->_overlayTextStageActive &&
+				!exec->_waitForPcmSound &&
+				!exec->_waitForMusicControl &&
+				!exec->_waitForAdlibReady &&
+				!exec->_waitForObjectAnimStep &&
+				!exec->_waitForSpecialAnimStep &&
+				!exec->_waitForDeltaAnim &&
+				!exec->_waitForDeltaSpeed &&
+				exec->canOpenSaveMenu()) {
 				if (ConfMan.getBool("original_menus")) {
-					_savedCursorMode = script->_cursorMode;
+					_savedCursorMode = exec->_cursorMode;
 					openOriginalSaveLoadPanel();
 				} else {
-					_savedCursorMode = script->_cursorMode;
+					_savedCursorMode = exec->_cursorMode;
 					g_engine->setCursorMode(Script::MouseMode::PanelCursor);
 					g_engine->openMainMenuDialog();
 					updateCursor();
@@ -2001,7 +2001,7 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 			return true;
 		}
 
-		if (script->_cursorMode == Script::MouseMode::Disabled) {
+		if (exec->_cursorMode == Script::MouseMode::Disabled) {
 			return true;
 		}
 		if (hasPersistentActionBar()) {
@@ -2009,7 +2009,7 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 			if (canCycleVerbs) {
 				g_engine->nextCursorMode();
 				_activeInventoryItem = nullptr;
-				script->_interactedInventoryItemId = 0;
+				exec->_interactedInventoryItemId = 0;
 				if (_actionBar && shouldShowActionBar())
 					_actionBar->syncActiveVerbFromCursorMode();
 				updateCursor();
@@ -2451,29 +2451,29 @@ bool View1::tick() {
 
 	// Music fade tick from gameTick (1008:e556).
 	// Processes volume fade in/out each frame when active.
-	Script::ScriptExecutor *se = g_engine->_scriptExecutor;
-	if (se->_activeMusicSlot != 0 && se->_musicControlMode != 0) {
-		const uint16 musicStep = MAX<uint16>(se->_musicControlStep, 1);
-		if (se->_musicControlMode == 1) {
+	Script::ScriptExecutor *exec = g_engine->_scriptExecutor;
+	if (exec->_activeMusicSlot != 0 && exec->_musicControlMode != 0) {
+		const uint16 musicStep = MAX<uint16>(exec->_musicControlStep, 1);
+		if (exec->_musicControlMode == 1) {
 			// Fade out: volume -= step
-			const int vol = (int)se->_musicControlVolume - (int)musicStep;
+			const int vol = (int)exec->_musicControlVolume - (int)musicStep;
 			if (vol < 1) {
-				se->_musicControlMode = 0;
-				se->_musicControlVolume = 0;
+				exec->_musicControlMode = 0;
+				exec->_musicControlVolume = 0;
 			} else {
-				se->_musicControlVolume = vol;
+				exec->_musicControlVolume = vol;
 			}
-			g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(se->_musicControlVolume));
+			g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(exec->_musicControlVolume));
 		} else {
 			// Fade in: volume += step. When >= 63: stop music.
-			const int vol = (int)se->_musicControlVolume + (int)musicStep;
+			const int vol = (int)exec->_musicControlVolume + (int)musicStep;
 			if (vol >= 0x3F) {
-				se->_musicControlMode = 0;
-				se->_activeMusicSlot = 0;
+				exec->_musicControlMode = 0;
+				exec->_activeMusicSlot = 0;
 				g_engine->getMusic()->stopMusic();
 			} else {
-				se->_musicControlVolume = vol;
-				g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(se->_musicControlVolume));
+				exec->_musicControlVolume = vol;
+				g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(exec->_musicControlVolume));
 			}
 		}
 	}
@@ -2551,29 +2551,27 @@ bool View1::tick() {
 	// Binary gameTick: drawScene during dialogue/text wait is gated; if movement
 	// finished fires while paused on a clickable wait, don't resume the script.
 	if (_uiPanelState == kUiPanelNone && !_isShowingDialoguePanel && !_isShowingTextBox) {
-		Script::ScriptExecutor *executor = g_engine->_scriptExecutor;
-
 		// Binary gameTick cascading if/else structure:
 		// if (frameWaitCounter == 0) { walkTarget / sound / music / adlib }
 		// else { drawScene(1); if counter==0 runScriptExecutor(); }
 		// Binary gameTick (1008:e556): each wait branch calls drawScene(1) before
 		// checking its completion flag and optionally resuming the script.
-		if (!executor->isFrameWaitActive()) {
+		if (!exec->isFrameWaitActive()) {
 			// Binary gameTick (1008:e752) walk-wait polling:
 			// When g_wWalkTargetObjectIndex > 0, check each frame if the character
 			// has reached its target position AND vertical offset matches.
 			// Binary uses exact equality: charPos == runtime.finalDest.
 			// This works because walkAlongPath snaps pos/finalDest on arrival.
-			uint16 walkTarget = executor->_walkTargetObjectIndex;
+			uint16 walkTarget = exec->_walkTargetObjectIndex;
 			if (walkTarget > 0) {
 				drawSceneUpdate();
 				GameObject *walkObject = GameObjects::getObjectByIndex(walkTarget);
 				if (walkObject == nullptr) {
-					executor->setScriptError(0x19);
-					executor->_walkTargetObjectIndex = 0;
+					exec->setScriptError(0x19);
+					exec->_walkTargetObjectIndex = 0;
 				} else if (walkObject->_dataOffset == 0) {
-					executor->setScriptError(2);
-					executor->_walkTargetObjectIndex = 0;
+					exec->setScriptError(2);
+					exec->_walkTargetObjectIndex = 0;
 				} else {
 					Character *c = getCharacterByIndex(walkTarget);
 					bool walkComplete = false;
@@ -2602,10 +2600,10 @@ bool View1::tick() {
 						}
 					}
 					if (walkComplete) {
-						if (!executor->_pickupInProgress) {
+						if (!exec->_pickupInProgress) {
 							debugC(kDebugScript, "waitForWalk complete obj=%u", walkTarget);
-							executor->debugLogActorWalkState("waitForWalk complete");
-							executor->_walkTargetObjectIndex = 0;
+							exec->debugLogActorWalkState("waitForWalk complete");
+							exec->_walkTargetObjectIndex = 0;
 							g_engine->runScriptExecutor();
 						} else if (c != nullptr && c->_gameObject->_orientation != OrientationPickup) {
 							// Binary: pickup in progress, trigger pickup animation.
@@ -2615,84 +2613,84 @@ bool View1::tick() {
 						}
 					}
 				}
-			} else if (executor->_waitForPcmSound) {
+			} else if (exec->_waitForPcmSound) {
 				drawSceneUpdate();
 				if (!g_engine->isSamplePlaying() && !g_engine->isSpeechPlaying()) {
 					debugC(kDebugScript, "waitForSound complete");
-					executor->debugLogActorWalkState("waitForSound complete");
-					executor->_waitForPcmSound = false;
+					exec->debugLogActorWalkState("waitForSound complete");
+					exec->_waitForPcmSound = false;
 					g_engine->getMusic()->setSmfDucked(false);
 					g_engine->runScriptExecutor();
 				}
-			} else if (executor->_waitForMusicControl) {
+			} else if (exec->_waitForMusicControl) {
 				drawSceneUpdate();
-				if (executor->_musicControlMode == 0) {
-					executor->_waitForMusicControl = false;
+				if (exec->_musicControlMode == 0) {
+					exec->_waitForMusicControl = false;
 					g_engine->runScriptExecutor();
 				}
-			} else if (executor->_waitForAdlibReady) {
+			} else if (exec->_waitForAdlibReady) {
 				drawSceneUpdate();
 				const Music *music = g_engine->getMusic();
 				const bool ready = music->isMidiFilePlaying() ? false : music->isPlaybackReady();
 				if (ready) {
-					executor->_waitForAdlibReady = false;
+					exec->_waitForAdlibReady = false;
 					g_engine->runScriptExecutor();
 				}
-			} else if (executor->_waitForObjectAnimStep) {
+			} else if (exec->_waitForObjectAnimStep) {
 				drawSceneUpdate();
 				bool animStepReached = false;
-				const GameObject *waitObject = GameObjects::getObjectByIndex(executor->_waitObjectAnimObjectId);
+				const GameObject *waitObject = GameObjects::getObjectByIndex(exec->_waitObjectAnimObjectId);
 				if (waitObject != nullptr && waitObject->_dataOffset != 0) {
-					const Common::Array<uint8> *blob = waitObject->getAnimSlotBlob(executor->_waitObjectAnimSlot);
+					const Common::Array<uint8> *blob = waitObject->getAnimSlotBlob(exec->_waitObjectAnimSlot);
 					if (blob != nullptr && !blob->empty()) {
 						AnimBlobView view(*blob);
 						if (view.isValid()) {
-							animStepReached = view.sequencePosition() >= executor->_waitObjectAnimTargetStep;
+							animStepReached = view.sequencePosition() >= exec->_waitObjectAnimTargetStep;
 						}
 					}
 				}
 				if (animStepReached) {
 					debugC(kDebugScript, "waitObjectAnimStep complete obj=%u slot=%u step=%u",
-						   executor->_waitObjectAnimObjectId, executor->_waitObjectAnimSlot,
-						   executor->_waitObjectAnimTargetStep);
-					executor->_waitForObjectAnimStep = false;
+						   exec->_waitObjectAnimObjectId, exec->_waitObjectAnimSlot,
+						   exec->_waitObjectAnimTargetStep);
+					exec->_waitForObjectAnimStep = false;
 					g_engine->runScriptExecutor();
 				}
-			} else if (executor->_waitForSpecialAnimStep) {
+			} else if (exec->_waitForSpecialAnimStep) {
 				drawSceneUpdate();
 				bool animStepReached = false;
-				const uint16 animIndex = executor->_waitSpecialAnimIndex;
+				const uint16 animIndex = exec->_waitSpecialAnimIndex;
 				if (animIndex > 0 && animIndex <= g_engine->_backgroundAnimationsBlobs.size()) {
 					const BackgroundAnimationBlob &blob = g_engine->_backgroundAnimationsBlobs[animIndex - 1];
 					const Common::Array<uint8> &active = blob.activeBlob();
 					if (!active.empty()) {
 						AnimBlobView view(active);
 						if (view.isValid())
-							animStepReached = view.sequencePosition() >= executor->_waitSpecialAnimTargetStep;
+							animStepReached = view.sequencePosition() >= exec->_waitSpecialAnimTargetStep;
 					}
 				}
 				if (animStepReached) {
 					debugC(kDebugScript, "waitSpecialAnimStep complete anim=%u step=%u",
-						   executor->_waitSpecialAnimIndex, executor->_waitSpecialAnimTargetStep);
-					executor->_waitForSpecialAnimStep = false;
+						   exec->_waitSpecialAnimIndex, exec->_waitSpecialAnimTargetStep);
+					exec->_waitForSpecialAnimStep = false;
 					g_engine->runScriptExecutor();
 				}
-			} else if (executor->_waitForDeltaAnim) {
+			} else if (exec->_waitForDeltaAnim) {
 				drawSceneUpdate();
 				if (!g_engine->tickDeltaPlayback()) {
 					debugC(kDebugScript, "waitForDeltaAnim complete");
-					executor->_waitForDeltaAnim = false;
+					exec->_waitForDeltaAnim = false;
 					_backgroundSurface.copyFrom(g_engine->_sceneBackground);
 					g_engine->runScriptExecutor();
 				} else {
 					_backgroundSurface.copyFrom(g_engine->_sceneBackground);
 					redraw();
 				}
-			} else if (executor->_waitForDeltaSpeed) {
+			} else if (exec->_waitForDeltaSpeed) {
 				drawSceneUpdate();
 				if (!g_engine->_deltaAnim.playing || !g_engine->tickDeltaPlayback()) {
 					debugC(kDebugScript, "waitForDeltaSpeed complete");
-					executor->_waitForDeltaSpeed = false;
+					exec->_waitForDeltaSpeed = false;
 					_backgroundSurface.copyFrom(g_engine->_sceneBackground);
 					g_engine->runScriptExecutor();
 				} else {
@@ -2702,10 +2700,10 @@ bool View1::tick() {
 			}
 		} else {
 			drawSceneUpdate();
-			if (executor->getFrameWaitCounter() == 0) {
+			if (exec->getFrameWaitCounter() == 0) {
 				debugC(kDebugScript, "frameWait complete");
-				executor->debugLogActorWalkState("frameWait complete");
-				executor->endFrameWait();
+				exec->debugLogActorWalkState("frameWait complete");
+				exec->endFrameWait();
 				g_engine->runScriptExecutor();
 			}
 		}
@@ -2723,13 +2721,12 @@ void View1::flushPendingCharacterDeletes() {
 }
 
 void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate) {
-	// drawAllCharacters @ 1008:90a2
 	g_engine->_movementFinishedFlag = false;
 	sortObjectListByY();
 	rebuildCharacterLookupTable();
 
 	const uint16 sortedCount = _sortedObjectCount;
-	Script::ScriptExecutor *executor = g_engine->_scriptExecutor;
+	Script::ScriptExecutor *exec = g_engine->_scriptExecutor;
 
 	if (fullUpdate && sortedCount > 0) {
 		for (uint16 local_c = 1; local_c <= sortedCount; local_c++) {
@@ -2767,7 +2764,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 				current = nullptr;
 
 			if (current != nullptr) {
-				if (obj->_orientation != OrientationPickup || executor->_pickupInProgress)
+				if (obj->_orientation != OrientationPickup || exec->_pickupInProgress)
 					current->update();
 			}
 		}
@@ -2779,7 +2776,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 	}
 
 	// draw sorted scene objects back -> front
-	if (surface != nullptr && !executor->hasScriptError() && sortedCount > 0) {
+	if (surface != nullptr && !exec->hasScriptError() && sortedCount > 0) {
 		const uint16 animAdvanceMode = (fullUpdate && _uiPanelState == kUiPanelNone) ? 2 : 0;
 
 		for (uint16 local_c = 1; local_c <= sortedCount; local_c++) {
@@ -2811,26 +2808,26 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 
 			const uint16 animSlot = g_engine->resolveAnimSlotIndex(obj);
 			if (!obj->isAnimSlotLoaded(animSlot)) {
-				executor->setScriptError(10);
+				exec->setScriptError(10);
 				return;
 			}
 
 			Common::Array<uint8> *blob = obj->getAnimSlotBlob(animSlot);
 			if (blob == nullptr || blob->empty()) {
-				executor->setScriptError(8);
+				exec->setScriptError(8);
 				return;
 			}
 
 			AnimBlobView blobView(*blob);
 			if (!blobView.isValid() || blobView.frameCount() == 0) {
-				executor->setScriptError(blobView.frameCount() == 0 ? 0x0B : 8);
+				exec->setScriptError(blobView.frameCount() == 0 ? 0x0B : 8);
 				return;
 			}
 
 			AnimFrame frame;
 			if (current != nullptr) {
 				if (!current->fillCurrentAnimationFrame(animAdvanceMode, frame)) {
-					executor->setScriptError(8);
+					exec->setScriptError(8);
 					return;
 				}
 			} else {
@@ -2958,22 +2955,22 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 
 	// Binary drawAllCharacters tail: movement-finished repeat run (opcode 0x27 area checks).
 	if (fullUpdate && g_engine->_movementFinishedFlag) {
-		if (executor->isScriptWaitDeferred()) {
+		if (exec->isScriptWaitDeferred()) {
 			debugC(kDebugScript,
 				   "repeatRun deferred: walkWait=%u frameWait=%u soundWait=%d musicWait=%d adlibWait=%d",
-				   executor->_walkTargetObjectIndex, executor->getFrameWaitCounter(),
-				   executor->_waitForPcmSound ? 1 : 0, executor->_waitForMusicControl ? 1 : 0,
-				   executor->_waitForAdlibReady ? 1 : 0);
+				   exec->_walkTargetObjectIndex, exec->getFrameWaitCounter(),
+				   exec->_waitForPcmSound ? 1 : 0, exec->_waitForMusicControl ? 1 : 0,
+				   exec->_waitForAdlibReady ? 1 : 0);
 		} else {
-			const Common::Point actorPos = executor->getCharPosition();
-			const uint16 area = executor->getAreaAtPoint(actorPos.x, actorPos.y);
+			const Common::Point actorPos = exec->getCharPosition();
+			const uint16 area = exec->getAreaAtPoint(actorPos.x, actorPos.y);
 			debugC(kDebugScript, "repeatRun start: actor=(%d,%d) areaRepeatRun=%u var[122]=%u",
-				   actorPos.x, actorPos.y, area, executor->getVariableValue(122));
-			executor->debugLogActorWalkState("repeatRun start");
-			executor->_isRepeatRun = true;
+				   actorPos.x, actorPos.y, area, exec->getVariableValue(122));
+			exec->debugLogActorWalkState("repeatRun start");
+			exec->_isRepeatRun = true;
 			g_engine->runScriptExecutor();
-			executor->_isRepeatRun = false;
-			executor->debugLogActorWalkState("repeatRun end");
+			exec->_isRepeatRun = false;
+			exec->debugLogActorWalkState("repeatRun end");
 		}
 	}
 }
@@ -4022,7 +4019,7 @@ bool Character::shouldStepVerticalMotion() const {
 }
 
 void Character::update() {
-	Script::ScriptExecutor *script = g_engine->_scriptExecutor;
+	Script::ScriptExecutor *exec = g_engine->_scriptExecutor;
 	if (_gameObject->_orientation == OrientationPickup) {
 		if (_pickedUpObject != nullptr) {
 			View1 *currentView = (View1 *)g_engine->findView("View1");
@@ -4034,17 +4031,17 @@ void Character::update() {
 
 			if (_pickupFrameCounter == _gameObject->_pickupFrameEnd) {
 				_gameObject->_orientation = _previousOrientation;
-				if (script->_pickupInProgress) {
-					script->_pickupInProgress = false;
-					script->_pickupActorObjectID = 0;
-					script->_pickupTargetObjectID = 0;
-					g_engine->setCursorMode(script->_cursorModeBeforeWait);
+				if (exec->_pickupInProgress) {
+					exec->_pickupInProgress = false;
+					exec->_pickupActorObjectID = 0;
+					exec->_pickupTargetObjectID = 0;
+					g_engine->setCursorMode(exec->_cursorModeBeforeWait);
 					currentView->updateCursor();
 				}
-				script->_walkTargetObjectIndex = 0;
+				exec->_walkTargetObjectIndex = 0;
 				_pickedUpObject = nullptr;
-				script->_interactedObjectID = 0x0000;
-				script->_interactedInventoryItemId = 0x0000;
+				exec->_interactedObjectID = 0x0000;
+				exec->_interactedInventoryItemId = 0x0000;
 				g_engine->_movementFinishedFlag = true;
 				return;
 			}
@@ -4208,7 +4205,7 @@ void Character::update() {
 		}
 		// Walkability check - binary uses getWalkabilityAt(posY, posX) >= 0xC8
 		if (!isWalkable(pos)) {
-			const uint16 tileArea = script->getAreaAtPoint(pos.x, pos.y);
+			const uint16 tileArea = exec->getAreaAtPoint(pos.x, pos.y);
 			if (tileArea >= 210 && tileArea <= 215) {
 				debugC(kDebugPath,
 						"walk blocked on plate area %u at (%d,%d) walk=%u int16=%d target=(%d,%d)",
@@ -4267,7 +4264,7 @@ void Character::update() {
 	}
 
 	if (pixelsMoved != walkSpeed) {
-		const uint16 tileArea = script->getAreaAtPoint(pos.x, pos.y);
+		const uint16 tileArea = exec->getAreaAtPoint(pos.x, pos.y);
 		if (tileArea >= 210 && tileArea <= 215) {
 			debugC(kDebugPath,
 				   "walk cancelled pixelsMoved=%d walkSpeed=%d at (%d,%d) area=%u walk=%u finalDest=(%d,%d)",
@@ -4562,7 +4559,7 @@ void View1::handleOriginalSaveLoadClick(const Common::Point &pos) {
 			hasData = (!frame._data.empty() && frame._width > 0);
 		}
 
-		Script::ScriptExecutor *scriptExecutor = g_engine->_scriptExecutor;
+		Script::ScriptExecutor *exec = g_engine->_scriptExecutor;
 		const bool isHit = (btnPos.x < clickX && btnPos.y < clickY &&
 					  clickX < btnPos.x + btnW && clickY < btnPos.y + btnH &&
 					  hasData &&
@@ -4580,7 +4577,7 @@ void View1::handleOriginalSaveLoadClick(const Common::Point &pos) {
 			// Process button action
 			if (i == 3) {
 				// Toggle music, reset clickedButton, redraw
-				scriptExecutor->_soundSystemActive = !scriptExecutor->_soundSystemActive;
+				exec->_soundSystemActive = !exec->_soundSystemActive;
 				_clickedButtonIndex = 0;
 				redraw();
 			} else if (i == 4) {
@@ -4590,27 +4587,27 @@ void View1::handleOriginalSaveLoadClick(const Common::Point &pos) {
 					_saveConfirmArmed = true;
 				} else {
 					// Binary: second click arms error 0x1C and closes via button 7
-					scriptExecutor->setScriptError(0x1C);
+					exec->setScriptError(0x1C);
 					_clickedButtonIndex = 7;
 				}
 			} else if (i == 6) {
 				if (!_loadConfirmArmed) {
 					_loadConfirmArmed = true;
 				} else {
-					scriptExecutor->setScriptError(0x1B);
+					exec->setScriptError(0x1B);
 					_clickedButtonIndex = 7;
 				}
 			} else if (i == 7) {
 				// Binary: if music enabled AND sound active, play active music
-				if (scriptExecutor->_musicEnabled &&
-					scriptExecutor->_soundSystemActive) {
-					const uint16 slot = scriptExecutor->_activeMusicSlot;
-					if (slot != 0 && !scriptExecutor->_musicSlots[slot - 1].empty() &&
-						g_engine->getMusic()->playSongData(scriptExecutor->_musicSlots[slot - 1])) {
+				if (exec->_musicEnabled &&
+					exec->_soundSystemActive) {
+					const uint16 slot = exec->_activeMusicSlot;
+					if (slot != 0 && !exec->_musicSlots[slot - 1].empty() &&
+						g_engine->getMusic()->playSongData(exec->_musicSlots[slot - 1])) {
 						// Original's adlibTickHandler resets g_bAdlibMasterVolume=0 (full volume).
 						// ScummVM layers user volume on top via scaledMusicVolume, so re-apply it.
-						scriptExecutor->_musicControlMode = 0;
-						scriptExecutor->_musicControlVolume = 0;
+						exec->_musicControlMode = 0;
+						exec->_musicControlVolume = 0;
 						g_engine->getMusic()->setVolume(g_engine->scaledMusicVolume(0));
 					}
 				}


Commit: 5476e7d8816928abc2446ad85a7893edeb14786d
    https://github.com/scummvm/scummvm/commit/5476e7d8816928abc2446ad85a7893edeb14786d
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-09-02T22:01:21+02:00

Commit Message:
MACS2: replaced magic numbers and removed comments

Changed paths:
    engines/macs2/view1.cpp


diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 629c4e5141e..41c49b406d8 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -4329,29 +4329,19 @@ void View1::openOriginalSaveLoadPanel() {
 		}
 	}
 
-	// g_wUiPanelWidth = (g_wActionBarButtonWidth + 10) * 7 + 4
 	uint16 panelWidth = (maxW + 10) * 7 + 4;
-	// if (g_wUiPanelWidth < 0xD4) g_wUiPanelWidth = 0xD4
 	if (panelWidth < 212) {
 		panelWidth = 212;
 	}
-	// g_wUiPanelHeight = g_wActionBarButtonHeight + 0x8A
 	const uint16 panelHeight = maxH + 138;
-	// g_wUiPanelX = (g_wScreenWidth >> 1) - (g_wUiPanelWidth >> 1)
 	const int panelX = 160 - (panelWidth >> 1);
-	// g_wUiPanelY = (g_wScreenHeight >> 1) - (g_wUiPanelHeight >> 1)
 	const int panelY = 100 - (panelHeight >> 1);
 
-	// g_wActionBarButtonWidth = g_wActionBarButtonWidth + 6
 	_saveLoadButtonWidth = maxW + 6;
-	// g_wActionBarButtonHeight = g_wActionBarButtonHeight + 6
 	_saveLoadButtonHeight = maxH + 6;
-
 	_saveLoadPanelRect = Common::Rect(panelX, panelY, panelX + panelWidth, panelY + panelHeight);
 
-	// local_6 = ((g_wScreenWidth >> 1) - (g_wActionBarButtonWidth + 4) * 7 / 2) + 2
 	int buttonRowX = (160 - (int)((_saveLoadButtonWidth + 4) * 7) / 2) + 2;
-	// local_8 = (g_wUiPanelY + g_wUiPanelHeight - 4) - g_wActionBarButtonHeight
 	const int buttonRowY = (panelY + panelHeight - 4) - _saveLoadButtonHeight;
 
 	// Second loop: store button positions and draw them
@@ -4464,14 +4454,12 @@ void View1::drawOriginalSaveLoadPanel(Graphics::ManagedSurface &s) {
 
 	// Slot loop: local_4 = 0..9
 	for (int slot = 0; slot <= 9; slot++) {
-		// drawPanelSlot(0xc, g_wUiPanelWidth - 8, g_wUiPanelY + 4 + slot * 0xc, g_wUiPanelX + 4)
+		const int slotH = 12;
 		const int slotX = panelX + 4;
-		const int slotY = panelY + 4 + slot * 12;
+		const int slotY = panelY + 4 + slot * slotH;
 		const int slotW = panelW - 8;
-		const int slotH = 12;
 		drawNinePatchBorder(Common::Point(slotX, slotY), Common::Point(slotW, slotH), kBorderPressed, false, false, s);
 
-		// drawText at (g_wUiPanelX + 6, g_wUiPanelY + 6 + slot * 0xc)
 		const int idx = _saveLoadPageIndex * 10 + slot;
 		Common::String label;
 		if (idx < ARRAYSIZE(_saveSlotNames) && !_saveSlotNames[idx].empty()) {
@@ -4482,7 +4470,7 @@ void View1::drawOriginalSaveLoadPanel(Graphics::ManagedSurface &s) {
 		}
 		const GlyphData *font = g_engine->numPanelGlyphs > 0 ? g_engine->_panelGlyphs : g_engine->_glyphs;
 		const uint16 fontCount = g_engine->numPanelGlyphs > 0 ? g_engine->numPanelGlyphs : g_engine->_numGlyphs;
-		renderStringWithFont(panelX + 6, panelY + 6 + slot * 12, label, font, fontCount);
+		renderStringWithFont(panelX + 6, panelY + 6 + slot * slotH, label, font, fontCount);
 	}
 }
 
@@ -4507,8 +4495,8 @@ void View1::handleOriginalSaveLoadClick(const Common::Point &pos) {
 		// TODO: drawSaveLoadScrollArrows
 	}
 
-	int clickX = pos.x;
-	int clickY = pos.y;
+	const int clickX = pos.x;
+	const int clickY = pos.y;
 	// Slot loop: local_4 = 0..9
 	for (int slot = 0; slot <= 9; slot++) {
 		// Slot hit test for sub-mode 2 (save): editSaveSlotName
@@ -4556,7 +4544,7 @@ void View1::handleOriginalSaveLoadClick(const Common::Point &pos) {
 		bool hasData = false;
 		if (imgIdx < (int)g_engine->_imageResources.size()) {
 			const AnimFrame &frame = g_engine->_imageResources[imgIdx];
-			hasData = (!frame._data.empty() && frame._width > 0);
+			hasData = !frame._data.empty() && frame._width > 0;
 		}
 
 		Script::ScriptExecutor *exec = g_engine->_scriptExecutor;
@@ -4599,8 +4587,7 @@ void View1::handleOriginalSaveLoadClick(const Common::Point &pos) {
 				}
 			} else if (i == 7) {
 				// Binary: if music enabled AND sound active, play active music
-				if (exec->_musicEnabled &&
-					exec->_soundSystemActive) {
+				if (exec->_musicEnabled && exec->_soundSystemActive) {
 					const uint16 slot = exec->_activeMusicSlot;
 					if (slot != 0 && !exec->_musicSlots[slot - 1].empty() &&
 						g_engine->getMusic()->playSongData(exec->_musicSlots[slot - 1])) {


Commit: 5ba656ca9eae1aaae479f508c79140ebb99e6c51
    https://github.com/scummvm/scummvm/commit/5ba656ca9eae1aaae479f508c79140ebb99e6c51
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-09-02T22:01:21+02:00

Commit Message:
MACS2: const + reduced code duplication

Changed paths:
    engines/macs2/view1.cpp


diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 41c49b406d8..29f3d89539a 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -3761,9 +3761,9 @@ bool Character::calculatePath(Common::Point target) {
 		int localBestCost = 0x7777;
 		int nextNode = currentNode;
 		for (uint a = 0; a < curPt._adjacentPoints.size(); a++) {
-			int adjIdx = curPt._adjacentPoints[a];
-			int cost = g_engine->computeMinCostToReachable(adjIdx, 0x7fff, _gameObject->_index, reachable, nodeCount, target);
-			int edgeCost = g_engine->walkableDistance(adjIdx, currentNode);
+			const int adjIdx = curPt._adjacentPoints[a];
+			const int cost = g_engine->computeMinCostToReachable(adjIdx, 0x7fff, _gameObject->_index, reachable, nodeCount, target);
+			const int edgeCost = g_engine->walkableDistance(adjIdx, currentNode);
 			if (cost + edgeCost < localBestCost) {
 				nextNode = adjIdx;
 				localBestCost = cost + edgeCost;
@@ -3937,25 +3937,24 @@ Macs2::AnimFrame *Character::getCurrentPortrait(bool onRightSide, uint16 frameIn
 	}
 
 	uint portraitBlobIndex = 17;
-	if (onRightSide && _gameObject->_blobs.size() > 18 && !_gameObject->_blobs[18].empty()) {
-		portraitBlobIndex = 18;
-	} else if (_gameObject->_blobs[portraitBlobIndex].empty() && _gameObject->_blobs.size() > 18 && !_gameObject->_blobs[18].empty()) {
-		portraitBlobIndex = 18;
+	if (_gameObject->_blobs.size() > 18 && !_gameObject->_blobs[18].empty()) {
+		if (onRightSide || _gameObject->_blobs[portraitBlobIndex].empty()) {
+			portraitBlobIndex = 18;
+		}
 	}
 
 	if (_gameObject->_blobs[portraitBlobIndex].empty()) {
 		return nullptr;
 	}
 
-	uint16 offset = BackgroundAnimationBlob::advanceAnimFrame(_gameObject->_blobs[portraitBlobIndex], true, frameIndex);
+	const uint16 offset = BackgroundAnimationBlob::advanceAnimFrame(_gameObject->_blobs[portraitBlobIndex], true, frameIndex);
 	// offset points to per-frame: offsetX(2), offsetY(2), unknown(2), width(2), height(2), pixels
-	offset += 6; // skip to width/height/pixels
 	Common::Array<uint8> &blob = _gameObject->_blobs[portraitBlobIndex];
 	AnimFrame *result = new AnimFrame();
-	result->_width = READ_LE_UINT16(&blob[offset]);
-	result->_height = READ_LE_UINT16(&blob[offset + 2]);
+	result->_width = READ_LE_UINT16(&blob[offset + 6]);
+	result->_height = READ_LE_UINT16(&blob[offset + 8]);
 	result->_data.resize(result->_width * result->_height);
-	memcpy(result->_data.data(), &blob[offset + 4], result->_width * result->_height);
+	memcpy(result->_data.data(), &blob[offset + 10], result->_width * result->_height);
 	return result;
 }
 
@@ -4040,8 +4039,8 @@ void Character::update() {
 				}
 				exec->_walkTargetObjectIndex = 0;
 				_pickedUpObject = nullptr;
-				exec->_interactedObjectID = 0x0000;
-				exec->_interactedInventoryItemId = 0x0000;
+				exec->_interactedObjectID = 0;
+				exec->_interactedInventoryItemId = 0;
 				g_engine->_movementFinishedFlag = true;
 				return;
 			}


Commit: e8c46eb17f8802448e5fbd53164d062eb1a3a54e
    https://github.com/scummvm/scummvm/commit/e8c46eb17f8802448e5fbd53164d062eb1a3a54e
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-09-02T22:01:21+02:00

Commit Message:
MACS2: extract character class into own file

Changed paths:
  A engines/macs2/character.cpp
  A engines/macs2/character.h
    engines/macs2/macs2.cpp
    engines/macs2/module.mk
    engines/macs2/view1.cpp
    engines/macs2/view1.h


diff --git a/engines/macs2/character.cpp b/engines/macs2/character.cpp
new file mode 100644
index 00000000000..b3b92f41fdb
--- /dev/null
+++ b/engines/macs2/character.cpp
@@ -0,0 +1,736 @@
+/* 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/character.h"
+#include "common/debug.h"
+#include "common/endian.h"
+#include "common/util.h"
+#include "macs2/detection.h"
+#include "macs2/events.h"
+#include "macs2/macs2.h"
+#include "macs2/macs2_constants.h"
+#include "macs2/scriptexecutor.h"
+#include "macs2/view1.h"
+
+namespace Macs2 {
+
+void resetCharacterWalkPath(Character *character) {
+	if (character == nullptr || character->_gameObject == nullptr) {
+		return;
+	}
+	const Common::Point &pos = character->getPosition();
+	character->_path.clear();
+	character->_currentPathIndex = 0;
+	character->_targetPosition = pos;
+	character->_pathFinalDestination = pos;
+	character->_stepDeltaX = 0;
+	character->_stepDeltaY = 0;
+	character->_stepError = 0;
+	character->_stepDirectionSet = false;
+}
+
+// Effective world position for pickup walk targets and bounds-attached props.
+// Inventory items use their holder's position; attached objects use parent + offset.
+static Common::Point getObjectEffectivePosition(const GameObject *object) {
+	if (object == nullptr) {
+		return Common::Point();
+	}
+	if (object->_hasBoundsAttachment) {
+		const GameObject *parent = GameObjects::getObjectByIndex(object->_boundsAttachmentObjectID);
+		if (parent != nullptr) {
+			return Common::Point(
+				parent->_position.x + (int16)object->_boundsAttachmentValue1,
+				parent->_position.y + (int16)object->_boundsAttachmentValue2);
+		}
+	}
+	if (object->_sceneIndex > 0x400) {
+		const GameObject *holder = GameObjects::getObjectByIndex(object->_sceneIndex - 0x400);
+		if (holder != nullptr) {
+			return holder->_position;
+		}
+	}
+	return object->_position;
+}
+
+bool Character::handleWalkability(Character *c) {
+	// Wall-sliding obstacle avoidance from walkAlongPath (1008:1b8f).
+	// When the character steps into a non-walkable pixel (walkability >= 200),
+	// the original code samples walkability at +/-1 and +/-2 pixels in each
+	// axis to build a gradient vector, then slides the character along that
+	// vector until it reaches a walkable position.
+	if (c->_gameObject->_index != 1) {
+		return false;
+	}
+	if (g_engine->_scriptExecutor->isExecuting()) {
+		return false;
+	}
+
+	Common::Point pos = c->getPosition();
+	if (isWalkable(pos)) {
+		return false;
+	}
+
+	// Build a push vector by sampling the walkability map around the current
+	// position. Non-walkable neighbors push us away from them.
+	int pushX = 0;
+	int pushY = 0;
+
+	// Sample at distance 1
+	if (!isWalkable(Common::Point(pos.x + 1, pos.y)))
+		pushX -= 1;
+	if (!isWalkable(Common::Point(pos.x - 1, pos.y)))
+		pushX += 1;
+	if (!isWalkable(Common::Point(pos.x, pos.y + 1)))
+		pushY -= 1;
+	if (!isWalkable(Common::Point(pos.x, pos.y - 1)))
+		pushY += 1;
+
+	// Sample at distance 2 for stronger gradient
+	if (!isWalkable(Common::Point(pos.x + 2, pos.y)))
+		pushX -= 1;
+	if (!isWalkable(Common::Point(pos.x - 2, pos.y)))
+		pushX += 1;
+	if (!isWalkable(Common::Point(pos.x, pos.y + 2)))
+		pushY -= 1;
+	if (!isWalkable(Common::Point(pos.x, pos.y - 2)))
+		pushY += 1;
+
+	// Slide along the push vector
+	while (pushX != 0 || pushY != 0) {
+		if (pushX < 0) {
+			if (isWalkable(Common::Point(pos.x - 1, pos.y)))
+				pos.x -= 1;
+			pushX += 1;
+		}
+		if (pushX > 0) {
+			if (isWalkable(Common::Point(pos.x + 1, pos.y)))
+				pos.x += 1;
+			pushX -= 1;
+		}
+		if (pushY < 0) {
+			if (isWalkable(Common::Point(pos.x, pos.y - 1)))
+				pos.y -= 1;
+			pushY += 1;
+		}
+		if (pushY > 0) {
+			if (isWalkable(Common::Point(pos.x, pos.y + 1)))
+				pos.y += 1;
+			pushY -= 1;
+		}
+	}
+
+	c->setPosition(pos);
+	return true;
+}
+
+uint16 Character::lookupWalkability(const Common::Point &p) const {
+	return g_engine->getWalkabilityAt((int16)p.y, (int16)p.x);
+}
+
+bool Character::isWalkable(const Common::Point &p) const {
+	return Macs2Engine::isWalkabilityWalkable(lookupWalkability(p));
+}
+
+Character::Character() : _pathfindingOverlay(g_engine->screenWidth() * g_engine->gameHeight(), 0) {
+}
+
+bool Character::calculatePath(Common::Point target) {
+	// Binary calculatePath (1008:1966). Params: charY, charX, finalDestY, finalDestX, actorIndex.
+	// The binary operates on the runtime struct directly; we store equivalent state in _path etc.
+	const Common::Point &charPos = _gameObject->_position;
+	const int nodeCount = g_engine->getPathfindingNodeCount();
+
+	// Step 1: Mark reachability anchored on FINAL DESTINATION (not character)
+	// scene[i + 0x50C2] = isPathWalkable(finalDest, node[i])
+	bool reachable[kPathNodeSlots + 1] = {};
+	for (int i = 1; i <= nodeCount; i++) {
+		const Common::Point &nodePos = g_engine->_pathfindingPoints[i - 1]._position;
+		reachable[i] = g_engine->isPathWalkable(target.y, target.x, nodePos.y, nodePos.x);
+	}
+
+	// Step 2: Find best entry node (lowest combined distance to both source and dest)
+	int bestCost = 0x7777;
+	int bestNode = 0;
+	for (int i = 1; i <= nodeCount; i++) {
+		const Common::Point &nodePos = g_engine->_pathfindingPoints[i - 1]._position;
+		int costToDest = g_engine->euclideanDistance(nodePos, target);
+		int costToChar = g_engine->euclideanDistance(nodePos, charPos);
+		if (costToDest + costToChar < bestCost) {
+			// Verify this node can connect source to target
+			// Binary calls canNodeConnectSourceToTarget(destY, destX, charY, charX, i)
+			// due to calculatePath being invoked with swapped source/dest params.
+			// This means the gate check is "can node see CHARACTER" and the flood-fill
+			// checks "any node reachable from DEST" AND "any node visible from CHARACTER".
+			// TODO: validate this with a playthought:
+			//   PVS-Studio V764: Possible incorrect order of arguments passed to
+			//   'canNodeConnectSourceToTarget' function: 'target' and 'charPos'.
+			// I didn't had any issues in previous runs
+			if (canNodeConnectSourceToTarget(i, target, charPos, reachable, nodeCount)) {
+				// Recompute cost (binary does this twice)
+				costToDest = g_engine->euclideanDistance(nodePos, target);
+				costToChar = g_engine->euclideanDistance(nodePos, charPos);
+				bestCost = costToDest + costToChar;
+				bestNode = i;
+			}
+		}
+	}
+
+	if (bestNode == 0) {
+		// No path found - go directly to target
+		// Binary: pathNodeCount=0, pathIndex=1
+		_path.clear();
+		_currentPathIndex = 1;
+		_targetPosition = target;
+		return false;
+	}
+
+	// Step 3: smoothPath - build path from bestNode toward a reachable node
+	_path.clear();
+	_path.push_back(bestNode);
+	int currentNode = bestNode;
+	while (!reachable[currentNode]) {
+		const PathfindingPoint &curPt = g_engine->_pathfindingPoints[currentNode - 1];
+		int localBestCost = 0x7777;
+		int nextNode = currentNode;
+		for (uint a = 0; a < curPt._adjacentPoints.size(); a++) {
+			const int adjIdx = curPt._adjacentPoints[a];
+			const int cost = g_engine->computeMinCostToReachable(adjIdx, 0x7fff, _gameObject->_index, reachable, nodeCount, target);
+			const int edgeCost = g_engine->walkableDistance(adjIdx, currentNode);
+			if (cost + edgeCost < localBestCost) {
+				nextNode = adjIdx;
+				localBestCost = cost + edgeCost;
+			}
+		}
+		currentNode = nextNode;
+		_path.push_back(currentNode);
+		if (_path.size() > kPathNodeSlots)
+			break; // safety
+	}
+
+	// Step 4: Validate path - consecutive nodes must be walkable to each other
+	for (uint i = 0; i + 1 < _path.size(); i++) {
+		const Common::Point &p1 = g_engine->_pathfindingPoints[_path[i + 1] - 1]._position;
+		const Common::Point &p2 = g_engine->_pathfindingPoints[_path[i] - 1]._position;
+		if (!g_engine->isPathWalkable(p1.y, p1.x, p2.y, p2.x)) {
+			// Path invalid - abort, go directly to target
+			_path.clear();
+			_targetPosition = target;
+			return false;
+		}
+	}
+
+	// Step 5: Skip-forward optimization - skip nodes the character can already reach directly.
+	// Binary: checks isPathWalkable(nextNode, charPos) - "can character see the next node?"
+	// Note: binary's calculatePath is called with swapped params, so its 'finalDest' param
+	// is actually the character position.
+	_currentPathIndex = 0;
+	while (_currentPathIndex + 1 < (int16)_path.size()) {
+		const Common::Point &nextNodePos = g_engine->_pathfindingPoints[_path[_currentPathIndex + 1] - 1]._position;
+		if (!g_engine->isPathWalkable(nextNodePos.y, nextNodePos.x, charPos.y, charPos.x))
+			break;
+		_currentPathIndex++;
+	}
+
+	// Set immediate target to the current path node
+	const Common::Point &firstTarget = g_engine->_pathfindingPoints[_path[_currentPathIndex] - 1]._position;
+	_targetPosition = firstTarget;
+	return true;
+}
+
+bool Character::canNodeConnectSourceToTarget(uint16 nodeIndex, const Common::Point &charPos, const Common::Point &target, const bool *reachable, int nodeCount) {
+	// Checks if node can connect source (charPos) to target:
+	// 1. Node must be able to see the target
+	// 2. Flood-fill connected component from node
+	// 3. Some node in component must see target AND some node must be seen from source
+	const Common::Point &nodePos = g_engine->_pathfindingPoints[nodeIndex - 1]._position;
+	if (!g_engine->isPathWalkable(nodePos.y, nodePos.x, target.y, target.x))
+		return false;
+
+	// Flood-fill connected nodes
+	bool visited[kPathNodeSlots + 1] = {};
+	floodFillConnectedNodes(nodeIndex, visited, nodeCount);
+
+	// Check both conditions
+	bool anySeesTarget = false;
+	bool anySeenFromSource = false;
+	for (int i = 1; i <= nodeCount; i++) {
+		if (!visited[i])
+			continue;
+		const Common::Point &p = g_engine->_pathfindingPoints[i - 1]._position;
+		if (g_engine->isPathWalkable(p.y, p.x, target.y, target.x))
+			anySeesTarget = true;
+		if (g_engine->isPathWalkable(charPos.y, charPos.x, p.y, p.x))
+			anySeenFromSource = true;
+	}
+	return anySeesTarget && anySeenFromSource;
+}
+
+void Character::floodFillConnectedNodes(int nodeIndex, bool *visited, int nodeCount) {
+	if (nodeIndex < 1 || nodeIndex > nodeCount)
+		return;
+	if (visited[nodeIndex])
+		return;
+	visited[nodeIndex] = true;
+	const PathfindingPoint &pt = g_engine->_pathfindingPoints[nodeIndex - 1];
+	for (uint i = 0; i < pt._adjacentPoints.size(); i++) {
+		floodFillConnectedNodes(pt._adjacentPoints[i], visited, nodeCount);
+	}
+}
+
+const Common::Point &Character::getPosition() const {
+	return _gameObject->_position;
+}
+
+void Character::setPosition(const Common::Point &newPosition) {
+	_gameObject->_position = newPosition;
+}
+
+uint16 Character::getVerticalOffset() const {
+	uint16 result = g_engine->getWalkabilityAt(getPosition());
+	if (Macs2Engine::isWalkabilityBlocking(result)) {
+		result = 0;
+	}
+
+	if (_gameObject->_verticalOffsetScale != 0) {
+		const int16 charY = getPosition().y;
+		const int32 depthOffset = ((int32)charY - (int32)g_engine->_walkDepthThresholdY) *
+							(int32)g_engine->_walkDepthScaleFactor / 100;
+		const uint16 scalingFactor = (uint16)((int32)g_engine->_walkBaseSpeedPct + depthOffset);
+		result = (scalingFactor * _gameObject->_verticalOffsetScale) / 100;
+	}
+
+	return result;
+}
+
+bool Character::walkAlongPath() {
+	if (_currentPathIndex >= 0 && _currentPathIndex < (int16)_path.size()) {
+		const uint16 snapIdx = _path[_currentPathIndex];
+		const Common::Point &snapPos = g_engine->_pathfindingPoints[snapIdx - 1]._position;
+		_gameObject->_position = snapPos;
+	}
+	_currentPathIndex++;
+	if (_currentPathIndex >= (int16)_path.size()) {
+		// Past end of path - walk to final destination, then stop
+		_targetPosition = _pathFinalDestination;
+		_stepDeltaX = (int16)ABS(_targetPosition.x - _gameObject->_position.x);
+		_stepDeltaY = (int16)ABS(_targetPosition.y - _gameObject->_position.y);
+		_stepError = 0;
+		_stepDirectionSet = false;
+		return false; // No more path segments after this
+	}
+	const uint16 nodeIdx = _path[_currentPathIndex];
+	const Common::Point &nodePos = g_engine->_pathfindingPoints[nodeIdx - 1]._position;
+	_targetPosition = nodePos;
+	_stepDeltaX = (int16)ABS(_targetPosition.x - _gameObject->_position.x);
+	_stepDeltaY = (int16)ABS(_targetPosition.y - _gameObject->_position.y);
+	_stepError = 0;
+	_stepDirectionSet = false;
+	return true;
+}
+
+bool Character::isAnimationMirrored() const {
+	switch (_gameObject->_orientation) {
+	case OrientationSouthWest:
+	case OrientationWest:
+	case OrientationNorthWest:
+	case OrientationStandingSouthWest:
+	case OrientationStandingWest:
+	case OrientationStandingNorthWest:
+		return true;
+	default:
+		break;
+	}
+	return false;
+}
+
+bool Character::fillCurrentAnimationFrame(uint16 advanceMode, Macs2::AnimFrame &out) const {
+	const uint16 animSlot = g_engine->resolveAnimSlotIndex(_gameObject);
+
+	Common::Array<uint8> *blobPtr = _gameObject->getAnimSlotBlob(animSlot);
+	if (blobPtr == nullptr || blobPtr->empty()) {
+		return false;
+	}
+
+	Common::Array<uint8> &blob = *blobPtr;
+	const uint16 frameStart = BackgroundAnimationBlob::advanceAnimFrame(blob, true, advanceMode);
+	out._offsetX = (int16)READ_LE_UINT16(&blob[frameStart]);
+	out._offsetY = (int16)READ_LE_UINT16(&blob[frameStart + 2]);
+	const uint16 offset = frameStart + 6;
+	out._width = READ_LE_UINT16(&blob[offset]);
+	out._height = READ_LE_UINT16(&blob[offset + 2]);
+	out._data.resize(out._width * out._height);
+	memcpy(out._data.data(), &blob[offset + 4], out._width * out._height);
+	return true;
+}
+
+Macs2::AnimFrame *Character::getCurrentPortrait(bool onRightSide, uint16 frameIndex) {
+	if (_gameObject->_blobs.size() <= 17) {
+		return nullptr;
+	}
+
+	uint portraitBlobIndex = 17;
+	if (_gameObject->_blobs.size() > 18 && !_gameObject->_blobs[18].empty()) {
+		if (onRightSide || _gameObject->_blobs[portraitBlobIndex].empty()) {
+			portraitBlobIndex = 18;
+		}
+	}
+
+	if (_gameObject->_blobs[portraitBlobIndex].empty()) {
+		return nullptr;
+	}
+
+	const uint16 offset = BackgroundAnimationBlob::advanceAnimFrame(_gameObject->_blobs[portraitBlobIndex], true, frameIndex);
+	// offset points to per-frame: offsetX(2), offsetY(2), unknown(2), width(2), height(2), pixels
+	Common::Array<uint8> &blob = _gameObject->_blobs[portraitBlobIndex];
+	AnimFrame *result = new AnimFrame();
+	result->_width = READ_LE_UINT16(&blob[offset + 6]);
+	result->_height = READ_LE_UINT16(&blob[offset + 8]);
+	result->_data.resize(result->_width * result->_height);
+	memcpy(result->_data.data(), &blob[offset + 10], result->_width * result->_height);
+	return result;
+}
+
+// Leftover lerp-era entry point. Duration and ignoreObstacles are unused.
+// Binary walkAlongPath (1008:1b8f) has no time lerp: Phase 0 sets 8-way
+// orientation and returns (1-frame delay); Phase 1 loops stepCounter 1..walkSpeed
+// with one-pixel Bresenham (error >= deltaX -> step Y else step X).
+// walkSpeed = animSpeed * (scene[0x5201] + depth) / 100, min 1;
+// depth = (posY - scene[0x51FD]) * scene[0x51FF] / 100.
+// Character::update() implements that. C++ walkAlongPath() is only the inlined
+// waypoint advance. Path setup is walkToScreenPosition / scriptWalkToPosition.
+void Character::startLerpTo(const Common::Point &target, uint32 duration, bool ignoreObstacles) {
+	_startPosition = getPosition();
+	_targetPosition = target;
+	_startTime = g_events->currentMillis;
+	_duration = duration;
+
+	// Reset Bresenham state - direction will be calculated on first Update()
+	_stepDirectionSet = false;
+	_stepDeltaX = (int16)ABS(_targetPosition.x - _startPosition.x);
+	_stepDeltaY = (int16)ABS(_targetPosition.y - _startPosition.y);
+	_stepError = 0;
+}
+
+void Character::startPickup(Macs2::GameObject *object) {
+	_pickedUpObject = object;
+	_pathFinalDestination = getObjectEffectivePosition(object);
+	_pickupFrameCounter = 0;
+	_pickupItemTransferred = false;
+
+	const Common::Point &current = getPosition();
+	const int16 destX = _pathFinalDestination.x;
+	const int16 destY = _pathFinalDestination.y;
+
+	_currentPathIndex = 0;
+	_path.clear();
+
+	const bool directPath = g_engine->isPathWalkable(destY, destX, current.y, current.x);
+	if (!directPath && Macs2Engine::isWalkabilityWalkable(g_engine->getWalkabilityAt(destY, destX))) {
+		calculatePath(Common::Point(destX, destY));
+	}
+
+	if (_path.empty()) {
+		_targetPosition = _pathFinalDestination;
+	}
+
+	_stepDeltaX = (int16)ABS(_targetPosition.x - current.x);
+	_stepDeltaY = (int16)ABS(_targetPosition.y - current.y);
+	_stepError = 0;
+	_stepDirectionSet = false;
+}
+
+bool Character::hasPendingVerticalMotion() const {
+	return (int16)_motionTargetVerticalOffset >= 0 &&
+		   _motionTargetVerticalOffset != _gameObject->_verticalOffsetScale;
+}
+
+bool Character::shouldStepVerticalMotion() const {
+	return (int16)_motionTargetVerticalOffset < 0 ||
+		   _motionTargetVerticalOffset != _gameObject->_verticalOffsetScale;
+}
+
+void Character::update() {
+	Script::ScriptExecutor *exec = g_engine->_scriptExecutor;
+	if (_gameObject->_orientation == OrientationPickup) {
+		if (_pickedUpObject != nullptr) {
+			View1 *currentView = (View1 *)g_engine->findView("View1");
+
+			if (!_pickupItemTransferred && _pickupFrameCounter == _gameObject->_pickupFrameStart) {
+				_pickupItemTransferred = true;
+				currentView->transferPickupTarget(_pickedUpObject);
+			}
+
+			if (_pickupFrameCounter == _gameObject->_pickupFrameEnd) {
+				_gameObject->_orientation = _previousOrientation;
+				if (exec->_pickupInProgress) {
+					exec->_pickupInProgress = false;
+					exec->_pickupActorObjectID = 0;
+					exec->_pickupTargetObjectID = 0;
+					g_engine->setCursorMode(exec->_cursorModeBeforeWait);
+					currentView->updateCursor();
+				}
+				exec->_walkTargetObjectIndex = 0;
+				_pickedUpObject = nullptr;
+				exec->_interactedObjectID = 0;
+				exec->_interactedInventoryItemId = 0;
+				g_engine->_movementFinishedFlag = true;
+				return;
+			}
+
+			_pickupFrameCounter++;
+		}
+		return;
+	}
+
+	Common::Point pos = getPosition();
+	const int32 depthOffset = ((int32)pos.y - (int32)g_engine->_walkDepthThresholdY) *
+							(int32)g_engine->_walkDepthScaleFactor / 100;
+	// Per-animation speed from blob data
+	// Walk speed from binary walkAlongPath
+	uint16 animSpeed = 2; // default fallback
+	ObjectOrientation orient = _gameObject->_orientation;
+	if (orient >= OrientationNorth && orient <= g_engine->maxAnimSlots() && (uint)(orient - 1) < _gameObject->_blobWalkSpeeds.size()) {
+		animSpeed = _gameObject->_blobWalkSpeeds[orient - 1];
+		if (animSpeed == 0) {
+			animSpeed = 2;
+		}
+	}
+	int walkSpeed = ((int)animSpeed * ((int)g_engine->_walkBaseSpeedPct + (int)depthOffset)) / 100;
+	if (walkSpeed < 1) {
+		walkSpeed = 1;
+	}
+
+	// Proximity arrival check from walkAlongPath
+	bool arrived = (ABS(pos.x - _targetPosition.x) <= walkSpeed) &&
+				   (ABS(pos.y - _targetPosition.y) <= walkSpeed);
+	if (arrived && hasPendingVerticalMotion()) {
+		arrived = false;
+	}
+	if (arrived) {
+		const bool atFinalDest = (_targetPosition.x == _pathFinalDestination.x &&
+							_targetPosition.y == _pathFinalDestination.y);
+
+		if (!atFinalDest && !_path.empty()) {
+			// Mid-path waypoint arrival: advance to next node
+			// Binary (23b0): snap pos to current path node, advance pathIndex
+			walkAlongPath();
+			return;
+		}
+
+		// Final destination arrival (or direct walk arrival)
+		if (_gameObject->_snapToTarget) {
+			setPosition(_targetPosition);
+			_pathFinalDestination = _targetPosition;
+		} else {
+			_targetPosition = pos;
+			_pathFinalDestination = pos;
+			if ((int16)_motionTargetVerticalOffset >= 0) {
+				_motionTargetVerticalOffset = _gameObject->_verticalOffsetScale;
+			}
+		}
+		_path.clear();
+		if (hasPendingVerticalMotion()) {
+			_gameObject->_verticalOffsetScale = _motionTargetVerticalOffset;
+			_motionProgress = _motionDistanceUnits;
+		}
+		// Walk arrival: orientation changes to standing (walking dir + 8).
+		// Script resumption is handled by position polling in View1::tick().
+		const bool wasWalking = (_gameObject->_orientation < OrientationStandingNorth);
+		if (wasWalking) {
+			_gameObject->_orientation = (ObjectOrientation)(_gameObject->_orientation + OrientationNorthWest);
+			g_engine->_movementFinishedFlag = true;
+		}
+		return;
+	}
+
+	// Binary: if target==current position, skip Phase 0 turn delay (set directionCalculated=1)
+	if (!_stepDirectionSet && _targetPosition.x == pos.x && _targetPosition.y == pos.y) {
+		_stepDirectionSet = true;
+	}
+
+	// Calculate direction if not yet set (first frame of movement)
+	if (!_stepDirectionSet) {
+		_stepDirectionSet = true;
+		// Phase 0 from walkAlongPath (1008:1b8f): direction calculation.
+		// Binary returns after setting direction (1-frame turn delay).
+		const uint16 absDx = (uint16)ABS(pos.x - _targetPosition.x);
+		const uint16 absDy = (uint16)ABS(pos.y - _targetPosition.y);
+		ObjectOrientation dir = _gameObject->_orientation;
+		if (dir >= OrientationStandingNorth && dir <= OrientationStandingNorthWest)
+			dir = (ObjectOrientation)(dir - OrientationNorthWest);
+		if (dir > OrientationStandingNorthWest)
+			dir = OrientationNorth;
+		// Cardinal directions (only if animation available for that direction)
+		if (_targetPosition.y < pos.y && absDx <= absDy &&
+			_gameObject->_blobs.size() > 0 && !_gameObject->_blobs[0].empty())
+			dir = OrientationNorth;
+		if (pos.x < _targetPosition.x && absDy <= absDx &&
+			_gameObject->_blobs.size() > 2 && !_gameObject->_blobs[2].empty())
+			dir = OrientationEast;
+		if (pos.y < _targetPosition.y && absDx <= absDy &&
+			_gameObject->_blobs.size() > 4 && !_gameObject->_blobs[4].empty())
+			dir = OrientationSouth;
+		if (_targetPosition.x < pos.x && absDy <= absDx &&
+			_gameObject->_blobs.size() > 6 && !_gameObject->_blobs[6].empty())
+			dir = OrientationWest;
+		// Diagonals: absDx/4 < absDy AND absDy/2 < absDx
+		if ((absDx >> 2) < absDy && (absDy >> 1) < absDx) {
+			if (_targetPosition.y < pos.y && pos.x < _targetPosition.x &&
+				_gameObject->_blobs.size() > 1 && !_gameObject->_blobs[1].empty())
+				dir = OrientationNorthEast;
+			if (pos.x < _targetPosition.x && pos.y < _targetPosition.y &&
+				_gameObject->_blobs.size() > 3 && !_gameObject->_blobs[3].empty())
+				dir = OrientationSouthEast;
+			if (pos.y < _targetPosition.y && _targetPosition.x < pos.x &&
+				_gameObject->_blobs.size() > 5 && !_gameObject->_blobs[5].empty())
+				dir = OrientationSouthWest;
+			if (_targetPosition.x < pos.x && _targetPosition.y < pos.y &&
+				_gameObject->_blobs.size() > 7 && !_gameObject->_blobs[7].empty())
+				dir = OrientationNorthWest;
+		}
+		_gameObject->_orientation = dir;
+		_stepDeltaX = (int16)absDx;
+		_stepDeltaY = (int16)absDy;
+		_stepError = 0;
+		// 1-frame turn delay: return after setting direction (binary Phase 0)
+		return;
+	}
+
+	// Phase 1: Bresenham stepping loop - exact 1:1 match of binary (1008:1ea1..2280)
+	// Binary: stepCounter from 1 to walkSpeed, NO early break. Loop always completes.
+	// After loop: if pixelsMoved != walkSpeed -> revert pos to savedPos and cancel path.
+	int pixelsMoved = 0;
+	Common::Point savedPos = pos;
+	for (int stepCounter = 1; stepCounter <= walkSpeed; stepCounter++) {
+		savedPos = pos; // Binary: savedX/savedY at top of each iteration
+		// Bresenham: if error >= deltaX -> step Y, else step X
+		if (_stepError >= _stepDeltaX) {
+			// Step Y axis
+			if (_targetPosition.y != pos.y)
+				pixelsMoved++;
+			if (_targetPosition.y < pos.y)
+				pos.y--;
+			else if (_targetPosition.y > pos.y)
+				pos.y++;
+			_stepError -= _stepDeltaX;
+		} else {
+			// Step X axis
+			if (_targetPosition.x != pos.x)
+				pixelsMoved++;
+			if (_targetPosition.x < pos.x)
+				pos.x--;
+			else if (_targetPosition.x > pos.x)
+				pos.x++;
+			_stepError += _stepDeltaY;
+		}
+		// Vertical offset interpolation
+		if (shouldStepVerticalMotion()) {
+			_motionProgress += _motionVerticalOffsetDelta;
+			while (_motionProgress >= _motionDistanceUnits && _motionDistanceUnits > 0) {
+				_motionProgress -= _motionDistanceUnits;
+				if (_motionTargetVerticalOffset < _gameObject->_verticalOffsetScale)
+					_gameObject->_verticalOffsetScale--;
+				else if (_motionTargetVerticalOffset > _gameObject->_verticalOffsetScale)
+					_gameObject->_verticalOffsetScale++;
+			}
+		}
+		// Walkability check - binary uses getWalkabilityAt(posY, posX) >= 0xC8
+		if (!isWalkable(pos)) {
+			const uint16 tileArea = exec->getAreaAtPoint(pos.x, pos.y);
+			if (tileArea >= 210 && tileArea <= 215) {
+				debugC(kDebugPath,
+						"walk blocked on plate area %u at (%d,%d) walk=%u int16=%d target=(%d,%d)",
+						tileArea, pos.x, pos.y, lookupWalkability(pos), (int16)lookupWalkability(pos),
+						_targetPosition.x, _targetPosition.y);
+			}
+			// Revert position
+			pos = savedPos;
+			// Wall-sliding: build push vector from +/-1 and +/-2 samples
+			int pushX = 0, pushY = 0;
+			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x + 1, pos.y))))
+				pushX--;
+			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x - 1, pos.y))))
+				pushX++;
+			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x, pos.y + 1))))
+				pushY--;
+			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x, pos.y - 1))))
+				pushY++;
+			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x + 2, pos.y))))
+				pushX--;
+			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x - 2, pos.y))))
+				pushX++;
+			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x, pos.y + 2))))
+				pushY--;
+			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x, pos.y - 2))))
+				pushY++;
+			// Apply push vector
+			while (pushX != 0 || pushY != 0) {
+				if (pushX < 0) {
+					if (Macs2Engine::isWalkabilityWalkable(lookupWalkability(Common::Point(pos.x - 1, pos.y))))
+						pos.x--;
+					pushX++;
+				}
+				if (pushX > 0) {
+					if (Macs2Engine::isWalkabilityWalkable(lookupWalkability(Common::Point(pos.x + 1, pos.y))))
+						pos.x++;
+					pushX--;
+				}
+				if (pushY < 0) {
+					if (Macs2Engine::isWalkabilityWalkable(lookupWalkability(Common::Point(pos.x, pos.y - 1))))
+						pos.y--;
+					pushY++;
+				}
+				if (pushY > 0) {
+					if (Macs2Engine::isWalkabilityWalkable(lookupWalkability(Common::Point(pos.x, pos.y + 1))))
+						pos.y++;
+					pushY--;
+				}
+			}
+			// Binary: target = finalDest = pos (cancel path, but loop continues)
+			_targetPosition = pos;
+			_pathFinalDestination = pos;
+			_path.clear();
+		}
+		// Binary: loop continues unconditionally until stepCounter == walkSpeed
+	}
+
+	if (pixelsMoved != walkSpeed) {
+		const uint16 tileArea = exec->getAreaAtPoint(pos.x, pos.y);
+		if (tileArea >= 210 && tileArea <= 215) {
+			debugC(kDebugPath,
+				   "walk cancelled pixelsMoved=%d walkSpeed=%d at (%d,%d) area=%u walk=%u finalDest=(%d,%d)",
+				   pixelsMoved, walkSpeed, pos.x, pos.y, tileArea, lookupWalkability(pos),
+				   _pathFinalDestination.x, _pathFinalDestination.y);
+		} else if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(pos))) {
+			debugC(kDebugPath,
+				   "walk cancelled (non-walkable) pixelsMoved=%d walkSpeed=%d at (%d,%d) walk=%u",
+				   pixelsMoved, walkSpeed, pos.x, pos.y, lookupWalkability(pos));
+		}
+		pos = savedPos;
+		_targetPosition = pos;
+		_pathFinalDestination = pos;
+		_path.clear();
+	}
+
+	setPosition(pos);
+}
+
+} // namespace Macs2
diff --git a/engines/macs2/character.h b/engines/macs2/character.h
new file mode 100644
index 00000000000..a32b1a56236
--- /dev/null
+++ b/engines/macs2/character.h
@@ -0,0 +1,107 @@
+/* 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_CHARACTER_H
+#define MACS2_CHARACTER_H
+
+#include "common/array.h"
+#include "common/rect.h"
+#include "macs2/gameobjects.h"
+
+namespace Macs2 {
+
+struct AnimFrame;
+
+class Character {
+private:
+	Common::Point _startPosition;
+
+	uint32 _startTime = 0;
+	uint32 _duration = 0;
+
+	// If this is set, a lerp to a location becomes picking up
+	Macs2::GameObject *_pickedUpObject = nullptr;
+
+	// Handle when the character has moved into a non-walkable area, push them out if
+	// they did and return true, return false otherwise
+	bool handleWalkability(Character *c);
+
+	uint16 lookupWalkability(const Common::Point &p) const;
+	bool shouldStepVerticalMotion() const;
+	bool isAnimationMirrored() const;
+	void floodFillConnectedNodes(int nodeIndex, bool *visited, int nodeCount);
+	// Returns false if we are at the end of the path already or the path is not valid
+	bool walkAlongPath();
+	void startLerpTo(const Common::Point &target, uint32 duration, bool ignoreObstacles = false);
+	bool isWalkable(const Common::Point &p) const;
+	bool canNodeConnectSourceToTarget(uint16 nodeIndex, const Common::Point &charPos, const Common::Point &target, const bool *reachable, int nodeCount);
+
+public:
+	Character();
+
+	// Frame counter for pickup animation (runtime+0x215).
+	// Increments each frame while orientation == 0x11.
+	// At _pickupFrameStart: item is transferred to inventory.
+	// At _pickupFrameEnd: animation ends, orientation restored.
+	uint16 _pickupFrameCounter = 0;
+	bool _pickupItemTransferred = false;
+	bool _markedForDeletion = false;
+
+	ObjectOrientation _previousOrientation = OrientationNone;
+
+	// Walk state from walkAlongPath (1008:1b8f) - runtime offsets +0x00..+0x0A, +0x18, +0x33
+	Common::Point _targetPosition;  // runtime[+0x00, +0x02]: next waypoint
+	int16 _stepDeltaX = 0;          // runtime[+0x04]: abs(endX - startX)
+	int16 _stepDeltaY = 0;          // runtime[+0x06]: abs(endY - startY)
+	int16 _stepError = 0;           // runtime[+0x18]: Bresenham error accumulator
+	bool _stepDirectionSet = false; // runtime[+0x33]: direction has been calculated
+
+	Common::Array<uint16> _path;
+	int16 _currentPathIndex = 0;
+	Common::Point _pathFinalDestination;
+	Common::Array<uint8> _pathfindingOverlay;
+
+	Macs2::GameObject *_gameObject = nullptr;
+	uint16 _motionTargetVerticalOffset = 0;
+	uint16 _motionVerticalOffsetDelta = 0;
+	uint16 _motionDistanceUnits = 0;
+	uint16 _motionProgress = 0;
+	uint16 _motionStartVerticalOffset = 0;
+
+	bool calculatePath(Common::Point target);
+	void startPickup(Macs2::GameObject *object);
+
+	const Common::Point &getPosition() const;
+	void setPosition(const Common::Point &newPosition);
+
+	uint16 getVerticalOffset() const;
+	bool hasPendingVerticalMotion() const;
+	bool fillCurrentAnimationFrame(uint16 advanceMode, Macs2::AnimFrame &out) const;
+	Macs2::AnimFrame *getCurrentPortrait(bool onRightSide = false, uint16 frameIndex = 0);
+
+	void update();
+};
+
+void resetCharacterWalkPath(Character *character);
+
+} // namespace Macs2
+
+#endif
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index e85135528eb..fec3334f6e3 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -82,21 +82,6 @@ Common::Point getSceneObjectHotspotPosition(View1 *view, GameObject *obj) {
 
 } // namespace
 
-void resetCharacterWalkPath(Character *character) {
-	if (character == nullptr || character->_gameObject == nullptr) {
-		return;
-	}
-	const Common::Point &pos = character->getPosition();
-	character->_path.clear();
-	character->_currentPathIndex = 0;
-	character->_targetPosition = pos;
-	character->_pathFinalDestination = pos;
-	character->_stepDeltaX = 0;
-	character->_stepDeltaY = 0;
-	character->_stepError = 0;
-	character->_stepDirectionSet = false;
-}
-
 Macs2Engine *g_engine;
 
 Graphics::ManagedSurface Macs2Engine::readRLEImage(int64 offs, Common::SeekableReadStream *stream) {
diff --git a/engines/macs2/module.mk b/engines/macs2/module.mk
index aedf8a2e2a6..4054b8fbd3b 100644
--- a/engines/macs2/module.mk
+++ b/engines/macs2/module.mk
@@ -4,6 +4,7 @@ MODULE_OBJS = \
 	amiga_archive.o \
 	amiga_decode.o \
 	amiga_resources.o \
+	character.o \
 	midiparser_macs2.o \
 	music.o \
 	dialogs.o \
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 29f3d89539a..4fb9d0376bc 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -1178,29 +1178,6 @@ int View1::getCharacterArrayIndex(const Character *c) const {
 	return -1;
 }
 
-// Effective world position for pickup walk targets and bounds-attached props.
-// Inventory items use their holder's position; attached objects use parent + offset.
-static Common::Point getObjectEffectivePosition(const GameObject *object) {
-	if (object == nullptr) {
-		return Common::Point();
-	}
-	if (object->_hasBoundsAttachment) {
-		const GameObject *parent = GameObjects::getObjectByIndex(object->_boundsAttachmentObjectID);
-		if (parent != nullptr) {
-			return Common::Point(
-				parent->_position.x + (int16)object->_boundsAttachmentValue1,
-				parent->_position.y + (int16)object->_boundsAttachmentValue2);
-		}
-	}
-	if (object->_sceneIndex > 0x400) {
-		const GameObject *holder = GameObjects::getObjectByIndex(object->_sceneIndex - 0x400);
-		if (holder != nullptr) {
-			return holder->_position;
-		}
-	}
-	return object->_position;
-}
-
 void View1::transferPickupTarget(GameObject *targetObject) {
 	if (targetObject == nullptr) {
 		return;
@@ -3620,669 +3597,6 @@ uint16 View1::getHitObjectID(const Common::Point &pos) const {
 	return 0;
 }
 
-bool Character::handleWalkability(Character *c) {
-	// Wall-sliding obstacle avoidance from walkAlongPath (1008:1b8f).
-	// When the character steps into a non-walkable pixel (walkability >= 200),
-	// the original code samples walkability at +/-1 and +/-2 pixels in each
-	// axis to build a gradient vector, then slides the character along that
-	// vector until it reaches a walkable position.
-	if (c->_gameObject->_index != 1) {
-		return false;
-	}
-	if (g_engine->_scriptExecutor->isExecuting()) {
-		return false;
-	}
-
-	Common::Point pos = c->getPosition();
-	if (isWalkable(pos)) {
-		return false;
-	}
-
-	// Build a push vector by sampling the walkability map around the current
-	// position. Non-walkable neighbors push us away from them.
-	int pushX = 0;
-	int pushY = 0;
-
-	// Sample at distance 1
-	if (!isWalkable(Common::Point(pos.x + 1, pos.y)))
-		pushX -= 1;
-	if (!isWalkable(Common::Point(pos.x - 1, pos.y)))
-		pushX += 1;
-	if (!isWalkable(Common::Point(pos.x, pos.y + 1)))
-		pushY -= 1;
-	if (!isWalkable(Common::Point(pos.x, pos.y - 1)))
-		pushY += 1;
-
-	// Sample at distance 2 for stronger gradient
-	if (!isWalkable(Common::Point(pos.x + 2, pos.y)))
-		pushX -= 1;
-	if (!isWalkable(Common::Point(pos.x - 2, pos.y)))
-		pushX += 1;
-	if (!isWalkable(Common::Point(pos.x, pos.y + 2)))
-		pushY -= 1;
-	if (!isWalkable(Common::Point(pos.x, pos.y - 2)))
-		pushY += 1;
-
-	// Slide along the push vector
-	while (pushX != 0 || pushY != 0) {
-		if (pushX < 0) {
-			if (isWalkable(Common::Point(pos.x - 1, pos.y)))
-				pos.x -= 1;
-			pushX += 1;
-		}
-		if (pushX > 0) {
-			if (isWalkable(Common::Point(pos.x + 1, pos.y)))
-				pos.x += 1;
-			pushX -= 1;
-		}
-		if (pushY < 0) {
-			if (isWalkable(Common::Point(pos.x, pos.y - 1)))
-				pos.y -= 1;
-			pushY += 1;
-		}
-		if (pushY > 0) {
-			if (isWalkable(Common::Point(pos.x, pos.y + 1)))
-				pos.y += 1;
-			pushY -= 1;
-		}
-	}
-
-	c->setPosition(pos);
-	return true;
-}
-
-uint16 Character::lookupWalkability(const Common::Point &p) const {
-	return g_engine->getWalkabilityAt((int16)p.y, (int16)p.x);
-}
-
-bool Character::isWalkable(const Common::Point &p) const {
-	return Macs2Engine::isWalkabilityWalkable(lookupWalkability(p));
-}
-
-Character::Character() : _pathfindingOverlay(g_engine->screenWidth() * g_engine->gameHeight(), 0) {
-}
-
-bool Character::calculatePath(Common::Point target) {
-	// Binary calculatePath (1008:1966). Params: charY, charX, finalDestY, finalDestX, actorIndex.
-	// The binary operates on the runtime struct directly; we store equivalent state in _path etc.
-	const Common::Point &charPos = _gameObject->_position;
-	const int nodeCount = g_engine->getPathfindingNodeCount();
-
-	// Step 1: Mark reachability anchored on FINAL DESTINATION (not character)
-	// scene[i + 0x50C2] = isPathWalkable(finalDest, node[i])
-	bool reachable[kPathNodeSlots + 1] = {};
-	for (int i = 1; i <= nodeCount; i++) {
-		const Common::Point &nodePos = g_engine->_pathfindingPoints[i - 1]._position;
-		reachable[i] = g_engine->isPathWalkable(target.y, target.x, nodePos.y, nodePos.x);
-	}
-
-	// Step 2: Find best entry node (lowest combined distance to both source and dest)
-	int bestCost = 0x7777;
-	int bestNode = 0;
-	for (int i = 1; i <= nodeCount; i++) {
-		const Common::Point &nodePos = g_engine->_pathfindingPoints[i - 1]._position;
-		int costToDest = g_engine->euclideanDistance(nodePos, target);
-		int costToChar = g_engine->euclideanDistance(nodePos, charPos);
-		if (costToDest + costToChar < bestCost) {
-			// Verify this node can connect source to target
-			// Binary calls canNodeConnectSourceToTarget(destY, destX, charY, charX, i)
-			// due to calculatePath being invoked with swapped source/dest params.
-			// This means the gate check is "can node see CHARACTER" and the flood-fill
-			// checks "any node reachable from DEST" AND "any node visible from CHARACTER".
-			// TODO: validate this with a playthought:
-			//   PVS-Studio V764: Possible incorrect order of arguments passed to
-			//   'canNodeConnectSourceToTarget' function: 'target' and 'charPos'.
-			// I didn't had any issues in previous runs
-			if (canNodeConnectSourceToTarget(i, target, charPos, reachable, nodeCount)) {
-				// Recompute cost (binary does this twice)
-				costToDest = g_engine->euclideanDistance(nodePos, target);
-				costToChar = g_engine->euclideanDistance(nodePos, charPos);
-				bestCost = costToDest + costToChar;
-				bestNode = i;
-			}
-		}
-	}
-
-	if (bestNode == 0) {
-		// No path found - go directly to target
-		// Binary: pathNodeCount=0, pathIndex=1
-		_path.clear();
-		_currentPathIndex = 1;
-		_targetPosition = target;
-		return false;
-	}
-
-	// Step 3: smoothPath - build path from bestNode toward a reachable node
-	_path.clear();
-	_path.push_back(bestNode);
-	int currentNode = bestNode;
-	while (!reachable[currentNode]) {
-		const PathfindingPoint &curPt = g_engine->_pathfindingPoints[currentNode - 1];
-		int localBestCost = 0x7777;
-		int nextNode = currentNode;
-		for (uint a = 0; a < curPt._adjacentPoints.size(); a++) {
-			const int adjIdx = curPt._adjacentPoints[a];
-			const int cost = g_engine->computeMinCostToReachable(adjIdx, 0x7fff, _gameObject->_index, reachable, nodeCount, target);
-			const int edgeCost = g_engine->walkableDistance(adjIdx, currentNode);
-			if (cost + edgeCost < localBestCost) {
-				nextNode = adjIdx;
-				localBestCost = cost + edgeCost;
-			}
-		}
-		currentNode = nextNode;
-		_path.push_back(currentNode);
-		if (_path.size() > kPathNodeSlots)
-			break; // safety
-	}
-
-	// Step 4: Validate path - consecutive nodes must be walkable to each other
-	for (uint i = 0; i + 1 < _path.size(); i++) {
-		const Common::Point &p1 = g_engine->_pathfindingPoints[_path[i + 1] - 1]._position;
-		const Common::Point &p2 = g_engine->_pathfindingPoints[_path[i] - 1]._position;
-		if (!g_engine->isPathWalkable(p1.y, p1.x, p2.y, p2.x)) {
-			// Path invalid - abort, go directly to target
-			_path.clear();
-			_targetPosition = target;
-			return false;
-		}
-	}
-
-	// Step 5: Skip-forward optimization - skip nodes the character can already reach directly.
-	// Binary: checks isPathWalkable(nextNode, charPos) - "can character see the next node?"
-	// Note: binary's calculatePath is called with swapped params, so its 'finalDest' param
-	// is actually the character position.
-	_currentPathIndex = 0;
-	while (_currentPathIndex + 1 < (int16)_path.size()) {
-		const Common::Point &nextNodePos = g_engine->_pathfindingPoints[_path[_currentPathIndex + 1] - 1]._position;
-		if (!g_engine->isPathWalkable(nextNodePos.y, nextNodePos.x, charPos.y, charPos.x))
-			break;
-		_currentPathIndex++;
-	}
-
-	// Set immediate target to the current path node
-	const Common::Point &firstTarget = g_engine->_pathfindingPoints[_path[_currentPathIndex] - 1]._position;
-	_targetPosition = firstTarget;
-	return true;
-}
-
-bool Character::canNodeConnectSourceToTarget(uint16 nodeIndex, const Common::Point &charPos, const Common::Point &target, const bool *reachable, int nodeCount) {
-	// Checks if node can connect source (charPos) to target:
-	// 1. Node must be able to see the target
-	// 2. Flood-fill connected component from node
-	// 3. Some node in component must see target AND some node must be seen from source
-	const Common::Point &nodePos = g_engine->_pathfindingPoints[nodeIndex - 1]._position;
-	if (!g_engine->isPathWalkable(nodePos.y, nodePos.x, target.y, target.x))
-		return false;
-
-	// Flood-fill connected nodes
-	bool visited[kPathNodeSlots + 1] = {};
-	floodFillConnectedNodes(nodeIndex, visited, nodeCount);
-
-	// Check both conditions
-	bool anySeesTarget = false;
-	bool anySeenFromSource = false;
-	for (int i = 1; i <= nodeCount; i++) {
-		if (!visited[i])
-			continue;
-		const Common::Point &p = g_engine->_pathfindingPoints[i - 1]._position;
-		if (g_engine->isPathWalkable(p.y, p.x, target.y, target.x))
-			anySeesTarget = true;
-		if (g_engine->isPathWalkable(charPos.y, charPos.x, p.y, p.x))
-			anySeenFromSource = true;
-	}
-	return anySeesTarget && anySeenFromSource;
-}
-
-void Character::floodFillConnectedNodes(int nodeIndex, bool *visited, int nodeCount) {
-	if (nodeIndex < 1 || nodeIndex > nodeCount)
-		return;
-	if (visited[nodeIndex])
-		return;
-	visited[nodeIndex] = true;
-	const PathfindingPoint &pt = g_engine->_pathfindingPoints[nodeIndex - 1];
-	for (uint i = 0; i < pt._adjacentPoints.size(); i++) {
-		floodFillConnectedNodes(pt._adjacentPoints[i], visited, nodeCount);
-	}
-}
-
-const Common::Point &Character::getPosition() const {
-	return _gameObject->_position;
-}
-
-void Character::setPosition(const Common::Point &newPosition) {
-	_gameObject->_position = newPosition;
-}
-
-uint16 Character::getVerticalOffset() const {
-	uint16 result = g_engine->getWalkabilityAt(getPosition());
-	if (Macs2Engine::isWalkabilityBlocking(result)) {
-		result = 0;
-	}
-
-	if (_gameObject->_verticalOffsetScale != 0) {
-		const int16 charY = getPosition().y;
-		const int32 depthOffset = ((int32)charY - (int32)g_engine->_walkDepthThresholdY) *
-							(int32)g_engine->_walkDepthScaleFactor / 100;
-		const uint16 scalingFactor = (uint16)((int32)g_engine->_walkBaseSpeedPct + depthOffset);
-		result = (scalingFactor * _gameObject->_verticalOffsetScale) / 100;
-	}
-
-	return result;
-}
-
-bool Character::walkAlongPath() {
-	if (_currentPathIndex >= 0 && _currentPathIndex < (int16)_path.size()) {
-		const uint16 snapIdx = _path[_currentPathIndex];
-		const Common::Point &snapPos = g_engine->_pathfindingPoints[snapIdx - 1]._position;
-		_gameObject->_position = snapPos;
-	}
-	_currentPathIndex++;
-	if (_currentPathIndex >= (int16)_path.size()) {
-		// Past end of path - walk to final destination, then stop
-		_targetPosition = _pathFinalDestination;
-		_stepDeltaX = (int16)ABS(_targetPosition.x - _gameObject->_position.x);
-		_stepDeltaY = (int16)ABS(_targetPosition.y - _gameObject->_position.y);
-		_stepError = 0;
-		_stepDirectionSet = false;
-		return false; // No more path segments after this
-	}
-	const uint16 nodeIdx = _path[_currentPathIndex];
-	const Common::Point &nodePos = g_engine->_pathfindingPoints[nodeIdx - 1]._position;
-	_targetPosition = nodePos;
-	_stepDeltaX = (int16)ABS(_targetPosition.x - _gameObject->_position.x);
-	_stepDeltaY = (int16)ABS(_targetPosition.y - _gameObject->_position.y);
-	_stepError = 0;
-	_stepDirectionSet = false;
-	return true;
-}
-
-bool Character::isAnimationMirrored() const {
-	switch (_gameObject->_orientation) {
-	case OrientationSouthWest:
-	case OrientationWest:
-	case OrientationNorthWest:
-	case OrientationStandingSouthWest:
-	case OrientationStandingWest:
-	case OrientationStandingNorthWest:
-		return true;
-	default:
-		break;
-	}
-	return false;
-}
-
-bool Character::fillCurrentAnimationFrame(uint16 advanceMode, Macs2::AnimFrame &out) const {
-	const uint16 animSlot = g_engine->resolveAnimSlotIndex(_gameObject);
-
-	Common::Array<uint8> *blobPtr = _gameObject->getAnimSlotBlob(animSlot);
-	if (blobPtr == nullptr || blobPtr->empty()) {
-		return false;
-	}
-
-	Common::Array<uint8> &blob = *blobPtr;
-	const uint16 frameStart = BackgroundAnimationBlob::advanceAnimFrame(blob, true, advanceMode);
-	out._offsetX = (int16)READ_LE_UINT16(&blob[frameStart]);
-	out._offsetY = (int16)READ_LE_UINT16(&blob[frameStart + 2]);
-	const uint16 offset = frameStart + 6;
-	out._width = READ_LE_UINT16(&blob[offset]);
-	out._height = READ_LE_UINT16(&blob[offset + 2]);
-	out._data.resize(out._width * out._height);
-	memcpy(out._data.data(), &blob[offset + 4], out._width * out._height);
-	return true;
-}
-
-Macs2::AnimFrame *Character::getCurrentPortrait(bool onRightSide, uint16 frameIndex) {
-	if (_gameObject->_blobs.size() <= 17) {
-		return nullptr;
-	}
-
-	uint portraitBlobIndex = 17;
-	if (_gameObject->_blobs.size() > 18 && !_gameObject->_blobs[18].empty()) {
-		if (onRightSide || _gameObject->_blobs[portraitBlobIndex].empty()) {
-			portraitBlobIndex = 18;
-		}
-	}
-
-	if (_gameObject->_blobs[portraitBlobIndex].empty()) {
-		return nullptr;
-	}
-
-	const uint16 offset = BackgroundAnimationBlob::advanceAnimFrame(_gameObject->_blobs[portraitBlobIndex], true, frameIndex);
-	// offset points to per-frame: offsetX(2), offsetY(2), unknown(2), width(2), height(2), pixels
-	Common::Array<uint8> &blob = _gameObject->_blobs[portraitBlobIndex];
-	AnimFrame *result = new AnimFrame();
-	result->_width = READ_LE_UINT16(&blob[offset + 6]);
-	result->_height = READ_LE_UINT16(&blob[offset + 8]);
-	result->_data.resize(result->_width * result->_height);
-	memcpy(result->_data.data(), &blob[offset + 10], result->_width * result->_height);
-	return result;
-}
-
-// Leftover lerp-era entry point. Duration and ignoreObstacles are unused.
-// Binary walkAlongPath (1008:1b8f) has no time lerp: Phase 0 sets 8-way
-// orientation and returns (1-frame delay); Phase 1 loops stepCounter 1..walkSpeed
-// with one-pixel Bresenham (error >= deltaX -> step Y else step X).
-// walkSpeed = animSpeed * (scene[0x5201] + depth) / 100, min 1;
-// depth = (posY - scene[0x51FD]) * scene[0x51FF] / 100.
-// Character::update() implements that. C++ walkAlongPath() is only the inlined
-// waypoint advance. Path setup is walkToScreenPosition / scriptWalkToPosition.
-void Character::startLerpTo(const Common::Point &target, uint32 duration, bool ignoreObstacles) {
-	_startPosition = getPosition();
-	_targetPosition = target;
-	_startTime = g_events->currentMillis;
-	_duration = duration;
-
-	// Reset Bresenham state - direction will be calculated on first Update()
-	_stepDirectionSet = false;
-	_stepDeltaX = (int16)ABS(_targetPosition.x - _startPosition.x);
-	_stepDeltaY = (int16)ABS(_targetPosition.y - _startPosition.y);
-	_stepError = 0;
-}
-
-void Character::startPickup(Macs2::GameObject *object) {
-	_pickedUpObject = object;
-	_pathFinalDestination = getObjectEffectivePosition(object);
-	_pickupFrameCounter = 0;
-	_pickupItemTransferred = false;
-
-	const Common::Point &current = getPosition();
-	const int16 destX = _pathFinalDestination.x;
-	const int16 destY = _pathFinalDestination.y;
-
-	_currentPathIndex = 0;
-	_path.clear();
-
-	const bool directPath = g_engine->isPathWalkable(destY, destX, current.y, current.x);
-	if (!directPath && Macs2Engine::isWalkabilityWalkable(g_engine->getWalkabilityAt(destY, destX))) {
-		calculatePath(Common::Point(destX, destY));
-	}
-
-	if (_path.empty()) {
-		_targetPosition = _pathFinalDestination;
-	}
-
-	_stepDeltaX = (int16)ABS(_targetPosition.x - current.x);
-	_stepDeltaY = (int16)ABS(_targetPosition.y - current.y);
-	_stepError = 0;
-	_stepDirectionSet = false;
-}
-
-bool Character::hasPendingVerticalMotion() const {
-	return (int16)_motionTargetVerticalOffset >= 0 &&
-		   _motionTargetVerticalOffset != _gameObject->_verticalOffsetScale;
-}
-
-bool Character::shouldStepVerticalMotion() const {
-	return (int16)_motionTargetVerticalOffset < 0 ||
-		   _motionTargetVerticalOffset != _gameObject->_verticalOffsetScale;
-}
-
-void Character::update() {
-	Script::ScriptExecutor *exec = g_engine->_scriptExecutor;
-	if (_gameObject->_orientation == OrientationPickup) {
-		if (_pickedUpObject != nullptr) {
-			View1 *currentView = (View1 *)g_engine->findView("View1");
-
-			if (!_pickupItemTransferred && _pickupFrameCounter == _gameObject->_pickupFrameStart) {
-				_pickupItemTransferred = true;
-				currentView->transferPickupTarget(_pickedUpObject);
-			}
-
-			if (_pickupFrameCounter == _gameObject->_pickupFrameEnd) {
-				_gameObject->_orientation = _previousOrientation;
-				if (exec->_pickupInProgress) {
-					exec->_pickupInProgress = false;
-					exec->_pickupActorObjectID = 0;
-					exec->_pickupTargetObjectID = 0;
-					g_engine->setCursorMode(exec->_cursorModeBeforeWait);
-					currentView->updateCursor();
-				}
-				exec->_walkTargetObjectIndex = 0;
-				_pickedUpObject = nullptr;
-				exec->_interactedObjectID = 0;
-				exec->_interactedInventoryItemId = 0;
-				g_engine->_movementFinishedFlag = true;
-				return;
-			}
-
-			_pickupFrameCounter++;
-		}
-		return;
-	}
-
-	Common::Point pos = getPosition();
-	const int32 depthOffset = ((int32)pos.y - (int32)g_engine->_walkDepthThresholdY) *
-							(int32)g_engine->_walkDepthScaleFactor / 100;
-	// Per-animation speed from blob data
-	// Walk speed from binary walkAlongPath
-	uint16 animSpeed = 2; // default fallback
-	ObjectOrientation orient = _gameObject->_orientation;
-	if (orient >= OrientationNorth && orient <= g_engine->maxAnimSlots() && (uint)(orient - 1) < _gameObject->_blobWalkSpeeds.size()) {
-		animSpeed = _gameObject->_blobWalkSpeeds[orient - 1];
-		if (animSpeed == 0) {
-			animSpeed = 2;
-		}
-	}
-	int walkSpeed = ((int)animSpeed * ((int)g_engine->_walkBaseSpeedPct + (int)depthOffset)) / 100;
-	if (walkSpeed < 1) {
-		walkSpeed = 1;
-	}
-
-	// Proximity arrival check from walkAlongPath
-	bool arrived = (ABS(pos.x - _targetPosition.x) <= walkSpeed) &&
-				   (ABS(pos.y - _targetPosition.y) <= walkSpeed);
-	if (arrived && hasPendingVerticalMotion()) {
-		arrived = false;
-	}
-	if (arrived) {
-		const bool atFinalDest = (_targetPosition.x == _pathFinalDestination.x &&
-							_targetPosition.y == _pathFinalDestination.y);
-
-		if (!atFinalDest && !_path.empty()) {
-			// Mid-path waypoint arrival: advance to next node
-			// Binary (23b0): snap pos to current path node, advance pathIndex
-			walkAlongPath();
-			return;
-		}
-
-		// Final destination arrival (or direct walk arrival)
-		if (_gameObject->_snapToTarget) {
-			setPosition(_targetPosition);
-			_pathFinalDestination = _targetPosition;
-		} else {
-			_targetPosition = pos;
-			_pathFinalDestination = pos;
-			if ((int16)_motionTargetVerticalOffset >= 0) {
-				_motionTargetVerticalOffset = _gameObject->_verticalOffsetScale;
-			}
-		}
-		_path.clear();
-		if (hasPendingVerticalMotion()) {
-			_gameObject->_verticalOffsetScale = _motionTargetVerticalOffset;
-			_motionProgress = _motionDistanceUnits;
-		}
-		// Walk arrival: orientation changes to standing (walking dir + 8).
-		// Script resumption is handled by position polling in View1::tick().
-		const bool wasWalking = (_gameObject->_orientation < OrientationStandingNorth);
-		if (wasWalking) {
-			_gameObject->_orientation = (ObjectOrientation)(_gameObject->_orientation + OrientationNorthWest);
-			g_engine->_movementFinishedFlag = true;
-		}
-		return;
-	}
-
-	// Binary: if target==current position, skip Phase 0 turn delay (set directionCalculated=1)
-	if (!_stepDirectionSet && _targetPosition.x == pos.x && _targetPosition.y == pos.y) {
-		_stepDirectionSet = true;
-	}
-
-	// Calculate direction if not yet set (first frame of movement)
-	if (!_stepDirectionSet) {
-		_stepDirectionSet = true;
-		// Phase 0 from walkAlongPath (1008:1b8f): direction calculation.
-		// Binary returns after setting direction (1-frame turn delay).
-		const uint16 absDx = (uint16)ABS(pos.x - _targetPosition.x);
-		const uint16 absDy = (uint16)ABS(pos.y - _targetPosition.y);
-		ObjectOrientation dir = _gameObject->_orientation;
-		if (dir >= OrientationStandingNorth && dir <= OrientationStandingNorthWest)
-			dir = (ObjectOrientation)(dir - OrientationNorthWest);
-		if (dir > OrientationStandingNorthWest)
-			dir = OrientationNorth;
-		// Cardinal directions (only if animation available for that direction)
-		if (_targetPosition.y < pos.y && absDx <= absDy &&
-			_gameObject->_blobs.size() > 0 && !_gameObject->_blobs[0].empty())
-			dir = OrientationNorth;
-		if (pos.x < _targetPosition.x && absDy <= absDx &&
-			_gameObject->_blobs.size() > 2 && !_gameObject->_blobs[2].empty())
-			dir = OrientationEast;
-		if (pos.y < _targetPosition.y && absDx <= absDy &&
-			_gameObject->_blobs.size() > 4 && !_gameObject->_blobs[4].empty())
-			dir = OrientationSouth;
-		if (_targetPosition.x < pos.x && absDy <= absDx &&
-			_gameObject->_blobs.size() > 6 && !_gameObject->_blobs[6].empty())
-			dir = OrientationWest;
-		// Diagonals: absDx/4 < absDy AND absDy/2 < absDx
-		if ((absDx >> 2) < absDy && (absDy >> 1) < absDx) {
-			if (_targetPosition.y < pos.y && pos.x < _targetPosition.x &&
-				_gameObject->_blobs.size() > 1 && !_gameObject->_blobs[1].empty())
-				dir = OrientationNorthEast;
-			if (pos.x < _targetPosition.x && pos.y < _targetPosition.y &&
-				_gameObject->_blobs.size() > 3 && !_gameObject->_blobs[3].empty())
-				dir = OrientationSouthEast;
-			if (pos.y < _targetPosition.y && _targetPosition.x < pos.x &&
-				_gameObject->_blobs.size() > 5 && !_gameObject->_blobs[5].empty())
-				dir = OrientationSouthWest;
-			if (_targetPosition.x < pos.x && _targetPosition.y < pos.y &&
-				_gameObject->_blobs.size() > 7 && !_gameObject->_blobs[7].empty())
-				dir = OrientationNorthWest;
-		}
-		_gameObject->_orientation = dir;
-		_stepDeltaX = (int16)absDx;
-		_stepDeltaY = (int16)absDy;
-		_stepError = 0;
-		// 1-frame turn delay: return after setting direction (binary Phase 0)
-		return;
-	}
-
-	// Phase 1: Bresenham stepping loop - exact 1:1 match of binary (1008:1ea1..2280)
-	// Binary: stepCounter from 1 to walkSpeed, NO early break. Loop always completes.
-	// After loop: if pixelsMoved != walkSpeed -> revert pos to savedPos and cancel path.
-	int pixelsMoved = 0;
-	Common::Point savedPos = pos;
-	for (int stepCounter = 1; stepCounter <= walkSpeed; stepCounter++) {
-		savedPos = pos; // Binary: savedX/savedY at top of each iteration
-		// Bresenham: if error >= deltaX -> step Y, else step X
-		if (_stepError >= _stepDeltaX) {
-			// Step Y axis
-			if (_targetPosition.y != pos.y)
-				pixelsMoved++;
-			if (_targetPosition.y < pos.y)
-				pos.y--;
-			else if (_targetPosition.y > pos.y)
-				pos.y++;
-			_stepError -= _stepDeltaX;
-		} else {
-			// Step X axis
-			if (_targetPosition.x != pos.x)
-				pixelsMoved++;
-			if (_targetPosition.x < pos.x)
-				pos.x--;
-			else if (_targetPosition.x > pos.x)
-				pos.x++;
-			_stepError += _stepDeltaY;
-		}
-		// Vertical offset interpolation
-		if (shouldStepVerticalMotion()) {
-			_motionProgress += _motionVerticalOffsetDelta;
-			while (_motionProgress >= _motionDistanceUnits && _motionDistanceUnits > 0) {
-				_motionProgress -= _motionDistanceUnits;
-				if (_motionTargetVerticalOffset < _gameObject->_verticalOffsetScale)
-					_gameObject->_verticalOffsetScale--;
-				else if (_motionTargetVerticalOffset > _gameObject->_verticalOffsetScale)
-					_gameObject->_verticalOffsetScale++;
-			}
-		}
-		// Walkability check - binary uses getWalkabilityAt(posY, posX) >= 0xC8
-		if (!isWalkable(pos)) {
-			const uint16 tileArea = exec->getAreaAtPoint(pos.x, pos.y);
-			if (tileArea >= 210 && tileArea <= 215) {
-				debugC(kDebugPath,
-						"walk blocked on plate area %u at (%d,%d) walk=%u int16=%d target=(%d,%d)",
-						tileArea, pos.x, pos.y, lookupWalkability(pos), (int16)lookupWalkability(pos),
-						_targetPosition.x, _targetPosition.y);
-			}
-			// Revert position
-			pos = savedPos;
-			// Wall-sliding: build push vector from +/-1 and +/-2 samples
-			int pushX = 0, pushY = 0;
-			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x + 1, pos.y))))
-				pushX--;
-			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x - 1, pos.y))))
-				pushX++;
-			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x, pos.y + 1))))
-				pushY--;
-			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x, pos.y - 1))))
-				pushY++;
-			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x + 2, pos.y))))
-				pushX--;
-			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x - 2, pos.y))))
-				pushX++;
-			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x, pos.y + 2))))
-				pushY--;
-			if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(Common::Point(pos.x, pos.y - 2))))
-				pushY++;
-			// Apply push vector
-			while (pushX != 0 || pushY != 0) {
-				if (pushX < 0) {
-					if (Macs2Engine::isWalkabilityWalkable(lookupWalkability(Common::Point(pos.x - 1, pos.y))))
-						pos.x--;
-					pushX++;
-				}
-				if (pushX > 0) {
-					if (Macs2Engine::isWalkabilityWalkable(lookupWalkability(Common::Point(pos.x + 1, pos.y))))
-						pos.x++;
-					pushX--;
-				}
-				if (pushY < 0) {
-					if (Macs2Engine::isWalkabilityWalkable(lookupWalkability(Common::Point(pos.x, pos.y - 1))))
-						pos.y--;
-					pushY++;
-				}
-				if (pushY > 0) {
-					if (Macs2Engine::isWalkabilityWalkable(lookupWalkability(Common::Point(pos.x, pos.y + 1))))
-						pos.y++;
-					pushY--;
-				}
-			}
-			// Binary: target = finalDest = pos (cancel path, but loop continues)
-			_targetPosition = pos;
-			_pathFinalDestination = pos;
-			_path.clear();
-		}
-		// Binary: loop continues unconditionally until stepCounter == walkSpeed
-	}
-
-	if (pixelsMoved != walkSpeed) {
-		const uint16 tileArea = exec->getAreaAtPoint(pos.x, pos.y);
-		if (tileArea >= 210 && tileArea <= 215) {
-			debugC(kDebugPath,
-				   "walk cancelled pixelsMoved=%d walkSpeed=%d at (%d,%d) area=%u walk=%u finalDest=(%d,%d)",
-				   pixelsMoved, walkSpeed, pos.x, pos.y, tileArea, lookupWalkability(pos),
-				   _pathFinalDestination.x, _pathFinalDestination.y);
-		} else if (Macs2Engine::isWalkabilityBlocking(lookupWalkability(pos))) {
-			debugC(kDebugPath,
-				   "walk cancelled (non-walkable) pixelsMoved=%d walkSpeed=%d at (%d,%d) walk=%u",
-				   pixelsMoved, walkSpeed, pos.x, pos.y, lookupWalkability(pos));
-		}
-		pos = savedPos;
-		_targetPosition = pos;
-		_pathFinalDestination = pos;
-		_path.clear();
-	}
-
-	setPosition(pos);
-}
-
 bool Button::isPointInside(const Common::Point &p) const {
 	return false;
 }
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index 6e30fa48b43..8a59beb7726 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -22,6 +22,7 @@
 #ifndef MACS2_VIEW1_H
 #define MACS2_VIEW1_H
 
+#include "macs2/character.h"
 #include "macs2/events.h"
 #include "macs2/gameobjects.h"
 #include "macs2/macs2.h"
@@ -53,78 +54,6 @@ public:
 	void render(Graphics::ManagedSurface &s);
 };
 
-class Character {
-private:
-	Common::Point _startPosition;
-
-	uint32 _startTime = 0;
-	uint32 _duration = 0;
-
-	// If this is set, a lerp to a location becomes picking up
-	Macs2::GameObject *_pickedUpObject = nullptr;
-
-	// Handle when the character has moved into a non-walkable area, push them out if
-	// they did and return true, return false otherwise
-	bool handleWalkability(Character *c);
-
-	uint16 lookupWalkability(const Common::Point &p) const;
-	bool shouldStepVerticalMotion() const;
-	bool isAnimationMirrored() const;
-	void floodFillConnectedNodes(int nodeIndex, bool *visited, int nodeCount);
-	// Returns false if we are at the end of the path already or the path is not valid
-	bool walkAlongPath();
-	void startLerpTo(const Common::Point &target, uint32 duration, bool ignoreObstacles = false);
-	bool isWalkable(const Common::Point &p) const;
-	bool canNodeConnectSourceToTarget(uint16 nodeIndex, const Common::Point &charPos, const Common::Point &target, const bool *reachable, int nodeCount);
-
-public:
-	Character();
-
-	// Frame counter for pickup animation (runtime+0x215).
-	// Increments each frame while orientation == 0x11.
-	// At _pickupFrameStart: item is transferred to inventory.
-	// At _pickupFrameEnd: animation ends, orientation restored.
-	uint16 _pickupFrameCounter = 0;
-	bool _pickupItemTransferred = false;
-	bool _markedForDeletion = false;
-
-	ObjectOrientation _previousOrientation = OrientationNone;
-
-	// Walk state from walkAlongPath (1008:1b8f) - runtime offsets +0x00..+0x0A, +0x18, +0x33
-	Common::Point _targetPosition;  // runtime[+0x00, +0x02]: next waypoint
-	int16 _stepDeltaX = 0;          // runtime[+0x04]: abs(endX - startX)
-	int16 _stepDeltaY = 0;          // runtime[+0x06]: abs(endY - startY)
-	int16 _stepError = 0;           // runtime[+0x18]: Bresenham error accumulator
-	bool _stepDirectionSet = false; // runtime[+0x33]: direction has been calculated
-
-	Common::Array<uint16> _path;
-	int16 _currentPathIndex = 0;
-	Common::Point _pathFinalDestination;
-	Common::Array<uint8> _pathfindingOverlay;
-
-	Macs2::GameObject *_gameObject = nullptr;
-	uint16 _motionTargetVerticalOffset = 0;
-	uint16 _motionVerticalOffsetDelta = 0;
-	uint16 _motionDistanceUnits = 0;
-	uint16 _motionProgress = 0;
-	uint16 _motionStartVerticalOffset = 0;
-
-	bool calculatePath(Common::Point target);
-	void startPickup(Macs2::GameObject *object);
-
-	const Common::Point &getPosition() const;
-	void setPosition(const Common::Point &newPosition);
-
-	uint16 getVerticalOffset() const;
-	bool hasPendingVerticalMotion() const;
-	bool fillCurrentAnimationFrame(uint16 advanceMode, Macs2::AnimFrame &out) const;
-	Macs2::AnimFrame *getCurrentPortrait(bool onRightSide = false, uint16 frameIndex = 0);
-
-	void update();
-};
-
-void resetCharacterWalkPath(Character *character);
-
 struct SpeechActData {
 	Character *speaker = nullptr;
 	Common::Array<Common::String> strings;


Commit: fcec8c98bfb83ab1dc71883d8d86f581ae4fb49e
    https://github.com/scummvm/scummvm/commit/fcec8c98bfb83ab1dc71883d8d86f581ae4fb49e
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-09-02T22:01:21+02:00

Commit Message:
MACS2: removed Button class

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


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index fec3334f6e3..343e6da93ed 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -685,7 +685,7 @@ void Macs2Engine::softRestart() {
 		currentView->_isShowingDialoguePanel = false;
 		currentView->_isDialogueChoiceInputActive = false;
 		currentView->_isShowingTextBox = false;
-		currentView->currentSpeechActData = SpeechActData();
+		currentView->_currentSpeechActData = SpeechActData();
 	}
 
 	for (uint i = 0; i < GameObjects::instance()._objects.size(); i++)
@@ -1521,7 +1521,7 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 	currentView->handleTextBoxInput();
 	currentView->_drawnStringBox.clear();
 	currentView->_continueScriptAfterUI = false;
-	currentView->currentSpeechActData = SpeechActData();
+	currentView->_currentSpeechActData = SpeechActData();
 	currentView->_pendingPanelRequest = View1::kPanelRequestNone;
 	currentView->_activeInventoryItem = nullptr;
 	currentView->_uiPanelState = View1::kUiPanelNone;
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 3f77b002e71..d1dfe05e985 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -433,7 +433,7 @@ void ScriptExecutor::scriptPrintString(bool alignRight) {
 		currentView->_stringBoxPosition = Common::Point(stringBoxX, stringBoxY);
 		currentView->_drawnStringBox = strings;
 		currentView->_isShowingTextBox = true;
-		currentView->currentSpeechActData.speaker = nullptr;
+		currentView->_currentSpeechActData.speaker = nullptr;
 		currentView->_continueScriptAfterUI = true;
 		_engine->sayText(joinTtsLines(strings), Common::TextToSpeechManager::INTERRUPT);
 		currentView->redraw();
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 4fb9d0376bc..6fcb9e6c9f7 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -224,7 +224,7 @@ const View1::BorderStyle View1::kBorderPressed = {0x1010, 0x1011, 0x1012};
 
 View1::View1() : UIElement("View1") {
 	_backgroundSurface.copyFrom(g_engine->_sceneBackground);
-	currentSpeechActData.onRightSide = false;
+	_currentSpeechActData.onRightSide = false;
 	updateCursor();
 	setViewPaletteSafely(g_engine->_pal);
 	_paletteDirty = false;
@@ -723,29 +723,29 @@ void View1::drawCurrentSpeaker(Graphics::ManagedSurface &s) {
 	// Cycles between frame 1 (mouth open) and frame 2 (mouth closed)
 	// based on a decrementing counter, creating a talking animation.
 	bool useAlternateBlob = false;
-	if (currentSpeechActData.mouthAnimActive) {
-		if (currentSpeechActData.mouthAnimCounter <= 0) {
+	if (_currentSpeechActData.mouthAnimActive) {
+		if (_currentSpeechActData.mouthAnimCounter <= 0) {
 			useAlternateBlob = true;
 		}
 	}
 
 	// Select portrait blob: primary (Blobs[17]) during countdown, alternate (Blobs[18]) after
 	// Mode 0: render current frame without advancing (advance happens in tick())
-	Common::ScopedPtr<AnimFrame> frame(currentSpeechActData.speaker->getCurrentPortrait(useAlternateBlob, 0));
+	Common::ScopedPtr<AnimFrame> frame(_currentSpeechActData.speaker->getCurrentPortrait(useAlternateBlob, 0));
 	if (!frame) {
 		return;
 	}
-	Common::ScopedPtr<AnimFrame> leftPortrait(currentSpeechActData.speaker->getCurrentPortrait(false, 0));
-	Common::ScopedPtr<AnimFrame> rightPortrait(currentSpeechActData.speaker->getCurrentPortrait(true, 0));
+	Common::ScopedPtr<AnimFrame> leftPortrait(_currentSpeechActData.speaker->getCurrentPortrait(false, 0));
+	Common::ScopedPtr<AnimFrame> rightPortrait(_currentSpeechActData.speaker->getCurrentPortrait(true, 0));
 
-	Common::Point pos = currentSpeechActData.position;
+	Common::Point pos = _currentSpeechActData.position;
 	if (!g_engine->isAmiga()) {
 		const int portraitWidth = MAX<int>(leftPortrait ? leftPortrait->_width : 0, rightPortrait ? rightPortrait->_width : 0);
 		const int portraitHeight = MAX<int>(leftPortrait ? leftPortrait->_height : 0, rightPortrait ? rightPortrait->_height : 0);
 		const int borderPad = g_engine->portraitBorderPad();
 		const int contentInset = g_engine->portraitContentInset();
 		const Common::Point borderSize(portraitWidth + borderPad, portraitHeight + borderPad);
-		drawBorder(currentSpeechActData.position, borderSize, s);
+		drawBorder(_currentSpeechActData.position, borderSize, s);
 		pos += Common::Point(contentInset, contentInset);
 	}
 	drawSprite(pos, frame->_width, frame->_height, frame->_data.data(), s, false);
@@ -2317,7 +2317,7 @@ void View1::draw() {
 				}
 			}
 		}
-		if (currentSpeechActData.speaker != nullptr) {
+		if (_currentSpeechActData.speaker != nullptr) {
 			drawCurrentSpeaker(s);
 		}
 	}
@@ -2478,16 +2478,16 @@ bool View1::tick() {
 	}
 
 	// Advance portrait animation once per tick
-	if (_isShowingDialoguePanel && currentSpeechActData.speaker != nullptr && currentSpeechActData.mouthAnimActive) {
-		Character *speaker = currentSpeechActData.speaker;
-		if (currentSpeechActData.mouthAnimCounter < 1) {
+	if (_isShowingDialoguePanel && _currentSpeechActData.speaker != nullptr && _currentSpeechActData.mouthAnimActive) {
+		Character *speaker = _currentSpeechActData.speaker;
+		if (_currentSpeechActData.mouthAnimCounter < 1) {
 			// counter < 1: advance alternate blob (Blobs[18]) with mode 2
 			if (speaker->_gameObject->_blobs.size() > 18 && !speaker->_gameObject->_blobs[18].empty()) {
 				BackgroundAnimationBlob::advanceAnimFrame(speaker->_gameObject->_blobs[18], true, 2);
 			}
 		} else {
-			currentSpeechActData.mouthAnimCounter--;
-			if (currentSpeechActData.mouthAnimCounter < 1) {
+			_currentSpeechActData.mouthAnimCounter--;
+			if (_currentSpeechActData.mouthAnimCounter < 1) {
 				// just hit 0: reset alternate blob (Blobs[18]) with mode 1
 				if (speaker->_gameObject->_blobs.size() > 18 && !speaker->_gameObject->_blobs[18].empty()) {
 					BackgroundAnimationBlob::advanceAnimFrame(speaker->_gameObject->_blobs[18], true, 1);
@@ -3330,10 +3330,10 @@ void View1::showSpeechAct(uint16 characterIndex, const Common::Array<Common::Str
 	_dialogueChoiceCount = 0;
 	_continueScriptAfterUI = true;
 
-	currentSpeechActData.speaker = getCharacterByIndex(characterIndex);
-	currentSpeechActData.strings = strings;
-	currentSpeechActData.position = position;
-	currentSpeechActData.onRightSide = onRightSide;
+	_currentSpeechActData.speaker = getCharacterByIndex(characterIndex);
+	_currentSpeechActData.strings = strings;
+	_currentSpeechActData.position = position;
+	_currentSpeechActData.onRightSide = onRightSide;
 
 	const int padW = g_engine->dialogPadW();
 	const int padH = g_engine->dialogPadH();
@@ -3344,9 +3344,9 @@ void View1::showSpeechAct(uint16 characterIndex, const Common::Array<Common::Str
 	int stringBoxY = position.y;
 	Common::Point portraitBoxPosition = position;
 
-	if (currentSpeechActData.speaker != nullptr) {
-		AnimFrame *leftPortrait = currentSpeechActData.speaker->getCurrentPortrait(false);
-		AnimFrame *rightPortrait = currentSpeechActData.speaker->getCurrentPortrait(true);
+	if (_currentSpeechActData.speaker != nullptr) {
+		AnimFrame *leftPortrait = _currentSpeechActData.speaker->getCurrentPortrait(false);
+		AnimFrame *rightPortrait = _currentSpeechActData.speaker->getCurrentPortrait(true);
 		const int portraitWidth = MAX<int>(leftPortrait ? leftPortrait->_width : 0, rightPortrait ? rightPortrait->_width : 0);
 		if (portraitWidth > 0) {
 			if (onRightSide) {
@@ -3360,19 +3360,19 @@ void View1::showSpeechAct(uint16 characterIndex, const Common::Array<Common::Str
 		delete rightPortrait;
 	}
 
-	currentSpeechActData.position = portraitBoxPosition;
+	_currentSpeechActData.position = portraitBoxPosition;
 	// Activate mouth animation (handleTimerCallback 1008:d38b)
-	currentSpeechActData.mouthAnimActive = (currentSpeechActData.speaker != nullptr);
+	_currentSpeechActData.mouthAnimActive = (_currentSpeechActData.speaker != nullptr);
 	// Original: PTR_LOOP_1020_1004 = sum of all line lengths (total character count)
 	int16 totalChars = 0;
 	for (const Common::String &line : strings) {
 		totalChars += line.size();
 	}
-	currentSpeechActData.mouthAnimCounter = (totalChars > 0) ? totalChars : 1;
+	_currentSpeechActData.mouthAnimCounter = (totalChars > 0) ? totalChars : 1;
 	_stringBoxPosition = Common::Point(stringBoxX, stringBoxY);
 	debugC(kDebugScript, "Layout speech act: speaker=%u rawPos=(%d,%d) rightSide=%u portraitBorderPos=(%d,%d) textBorderPos=(%d,%d) textBorderSize=(%d,%d) text=\"%s\"",
 		   characterIndex, position.x, position.y, onRightSide ? 1 : 0,
-		   currentSpeechActData.position.x, currentSpeechActData.position.y,
+		   _currentSpeechActData.position.x, _currentSpeechActData.position.y,
 		   _stringBoxPosition.x, _stringBoxPosition.y, totalWidth, totalHeight, joinDebugStrings(strings).c_str());
 
 	if (_autoclickActive) {
@@ -3597,13 +3597,6 @@ uint16 View1::getHitObjectID(const Common::Point &pos) const {
 	return 0;
 }
 
-bool Button::isPointInside(const Common::Point &p) const {
-	return false;
-}
-
-void Button::render(Graphics::ManagedSurface &s) {
-}
-
 void View1::openOriginalSaveLoadPanel() {
 	_pendingPanelRequest = kPanelRequestNone;
 	_uiPanelState = kUiPanelSaveLoad;
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index 8a59beb7726..ae8541dd33a 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -43,17 +43,6 @@ enum class FadeMode {
 	ToBlack
 };
 
-class Button {
-public:
-	Common::Point _position;
-	Common::Point _size;
-	Common::String _caption;
-
-	bool isPointInside(const Common::Point &p) const;
-
-	void render(Graphics::ManagedSurface &s);
-};
-
 struct SpeechActData {
 	Character *speaker = nullptr;
 	Common::Array<Common::String> strings;
@@ -239,7 +228,7 @@ public:
 	Common::StringArray _drawnStringBox;
 	uint16 _dialogueChoiceCount = 0;
 	Common::Array<uint16> _dialogueChoiceLineCounts;
-	SpeechActData currentSpeechActData;
+	SpeechActData _currentSpeechActData;
 	Graphics::ManagedSurface _backgroundSurface;
 
 	bool _started = false;




More information about the Scummvm-git-logs mailing list