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

alexbevi noreply at scummvm.org
Sat Oct 17 12:38:37 UTC 2026


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

Summary:
1d9db0c28e HARVESTER: get rid of duplicated clipped transparent bitmap implementations
19926d2016 HARVESTER: add text editing helpers
41f2eba3b0 HARVESTER: refactor save/load to use a common slot system
a8a11fd66c HARVESTER: refactor NPC dialogue handling
8ae6a2999b HARVESTER: refactor player state management
852752b953 HARVESTER: improve Combat design
a99abfd26a HARVESTER: add an explicit execution context for scripts
4c2cd50a79 HARVESTER: Move font lookup into Text
736efc4b9d HARVESTER: Move scene drawing into RoomPresentation
3099689314 HARVESTER: Replace Flow access bridge with explicit operations
704302dcdc HARVESTER: Encapsulate remaining Flow state access
102331f518 HARVESTER: Remove remaining Flow friendships
bad29b7c89 HARVESTER: Share room object entity construction
41fbd50e02 HARVESTER: Share NPC animation configuration
0c2252ad15 HARVESTER: Share monster entity construction
db94aa0a4e HARVESTER: Share region entity construction
88ff885adb HARVESTER: Move inspect text rendering into RoomPresentation
9f90244baa HARVESTER: Move scene record lookups into SceneEntities
fb53fc9838 HARVESTER: Move scene resource assembly into SceneEntities
bdb15ec5f3 HARVESTER: Move hover resolution into Input
d092cf2688 HARVESTER: Fix cursor freezing after idle animations


Commit: 1d9db0c28ecfdc324afae1e95375d7df6dda0e12
    https://github.com/scummvm/scummvm/commit/1d9db0c28ecfdc324afae1e95375d7df6dda0e12
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:06-04:00

Commit Message:
HARVESTER: get rid of duplicated clipped transparent bitmap implementations

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/art.cpp
    engines/harvester/art.h
    engines/harvester/bitmap.cpp
    engines/harvester/bitmap.h
    engines/harvester/dialogue.cpp
    engines/harvester/entity.cpp
    engines/harvester/flow.cpp
    engines/harvester/inventory.cpp
    engines/harvester/menu.cpp
    engines/harvester/npc/chessmaster_dialogue.cpp
    engines/harvester/room_presentation.cpp
    engines/harvester/room_presentation.h
    engines/harvester/scene.cpp


diff --git a/engines/harvester/art.cpp b/engines/harvester/art.cpp
index aab5aed1950..702c04b1966 100644
--- a/engines/harvester/art.cpp
+++ b/engines/harvester/art.cpp
@@ -22,7 +22,6 @@
 #include "harvester/art.h"
 
 #include "common/debug.h"
-#include "graphics/blit.h"
 #include "graphics/screen.h"
 #include "harvester/detection.h"
 #include "harvester/palette_utils.h"
@@ -196,41 +195,6 @@ bool Art::loadAnimation(ResourceManager &resources, const Common::String &path,
 	return true;
 }
 
-void Art::blitTransparentBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) const {
-	if (!bitmap.isValid())
-		return;
-
-	int srcX = 0;
-	int srcY = 0;
-	int destX = x;
-	int destY = y;
-	int width = (int)bitmap.width;
-	int height = (int)bitmap.height;
-
-	if (destX < 0) {
-		srcX = -destX;
-		width += destX;
-		destX = 0;
-	}
-	if (destY < 0) {
-		srcY = -destY;
-		height += destY;
-		destY = 0;
-	}
-	if (destX >= screen.w || destY >= screen.h || width <= 0 || height <= 0)
-		return;
-
-	width = MIN<int>(width, screen.w - destX);
-	height = MIN<int>(height, screen.h - destY);
-	if (width <= 0 || height <= 0)
-		return;
-
-	const byte *src = bitmap.pixels.data() + srcY * bitmap.width + srcX;
-	byte *dst = (byte *)screen.getBasePtr(destX, destY);
-	Graphics::keyBlit(dst, src, screen.pitch, bitmap.width, width, height,
-		screen.format.bytesPerPixel, kTransparentPaletteIndex);
-}
-
 void Art::blitTransparentAnimationFrame(Graphics::Screen &screen, const Common::Array<AbmFrame> &frames,
 		uint frameIndex, int x, int y) const {
 	if (frameIndex >= frames.size() || !frames[frameIndex].isValid())
diff --git a/engines/harvester/art.h b/engines/harvester/art.h
index 3877523750e..1b810560a21 100644
--- a/engines/harvester/art.h
+++ b/engines/harvester/art.h
@@ -53,7 +53,6 @@ private:
 	bool loadPalette(ResourceManager &resources, const Common::String &path, byte *dest) const;
 	bool loadBitmap(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap) const;
 	bool loadAnimation(ResourceManager &resources, const Common::String &path, Common::Array<AbmFrame> &frames) const;
-	void blitTransparentBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) const;
 	void blitTransparentAnimationFrame(Graphics::Screen &screen, const Common::Array<AbmFrame> &frames,
 		uint frameIndex, int x, int y) const;
 
diff --git a/engines/harvester/bitmap.cpp b/engines/harvester/bitmap.cpp
index 1d1d8df4143..42abf72d726 100644
--- a/engines/harvester/bitmap.cpp
+++ b/engines/harvester/bitmap.cpp
@@ -23,10 +23,24 @@
 
 #include "common/debug.h"
 #include "common/endian.h"
+#include "graphics/managed_surface.h"
 #include "harvester/detection.h"
 
 namespace Harvester {
 
+void blitTransparentBitmap(Graphics::ManagedSurface &surface, const IndexedBitmap &bitmap, int x, int y) {
+	if (!bitmap.isValid() || x >= surface.w || y >= surface.h ||
+			x <= -(int)bitmap.width || y <= -(int)bitmap.height)
+		return;
+
+	Graphics::ManagedSurface source;
+	// Borrow the bitmap pixels without taking ownership.
+	source.surfacePtr()->init(bitmap.width, bitmap.height, bitmap.width,
+		const_cast<byte *>(bitmap.pixels.data()), Graphics::PixelFormat::createFormatCLUT8());
+	source.setTransparentColor(0);
+	surface.simpleBlitFrom(source, Common::Point(x, y));
+}
+
 namespace {
 
 static bool decodeAnimationFrame(const byte *source, uint32 sourceSize, bool compressed, Common::Array<byte> &dest) {
diff --git a/engines/harvester/bitmap.h b/engines/harvester/bitmap.h
index 9a87c2cbcaa..40cc0a26066 100644
--- a/engines/harvester/bitmap.h
+++ b/engines/harvester/bitmap.h
@@ -24,6 +24,10 @@
 
 #include "common/array.h"
 
+namespace Graphics {
+class ManagedSurface;
+}
+
 namespace Harvester {
 
 struct IndexedBitmap {
@@ -41,6 +45,9 @@ struct AbmFrame : IndexedBitmap {
 	int32 yOffset = 0;
 };
 
+// Draw unscaled pixels with index 0 transparent; callers supply the final position.
+void blitTransparentBitmap(Graphics::ManagedSurface &surface, const IndexedBitmap &bitmap, int x, int y);
+
 // Callers choose when to clear output. Failed decoding can leave partial header or frame data.
 bool decodeBitmap(const Common::Array<byte> &data, IndexedBitmap &bitmap);
 bool decodeAbm(const Common::Array<byte> &data, Common::Array<AbmFrame> &frames);
diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index b0a65e07877..4835f476fe5 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -26,7 +26,6 @@
 #include "common/events.h"
 #include "common/ptr.h"
 #include "common/system.h"
-#include "graphics/blit.h"
 #include "graphics/font.h"
 #include "graphics/fontman.h"
 #include "graphics/framelimiter.h"
@@ -150,7 +149,6 @@ static const char *const kDialogueLocalizedKeywordBitmapPath = "GRAPHIC/OTHER/KE
 static const byte kTextColorNormal = 255;
 static const byte kTextColorHover = 251;
 static const byte kShadowColor = 0;
-static const byte kTransparentPaletteIndex = 0;
 
 static bool isDialogueVoiceInterruptEvent(const Common::Event &event) {
 	return event.type == Common::EVENT_LBUTTONDOWN ||
@@ -229,41 +227,6 @@ static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, in
 	screen.copyRectToSurface(bitmap.pixels.data(), bitmap.width, x, y, bitmap.width, bitmap.height);
 }
 
-static void blitTransparentBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
-	if (!bitmap.isValid())
-		return;
-
-	int srcX = 0;
-	int srcY = 0;
-	int destX = x;
-	int destY = y;
-	int width = (int)bitmap.width;
-	int height = (int)bitmap.height;
-
-	if (destX < 0) {
-		srcX = -destX;
-		width += destX;
-		destX = 0;
-	}
-	if (destY < 0) {
-		srcY = -destY;
-		height += destY;
-		destY = 0;
-	}
-	if (destX >= screen.w || destY >= screen.h || width <= 0 || height <= 0)
-		return;
-
-	width = MIN<int>(width, screen.w - destX);
-	height = MIN<int>(height, screen.h - destY);
-	if (width <= 0 || height <= 0)
-		return;
-
-	const byte *src = bitmap.pixels.data() + srcY * bitmap.width + srcX;
-	byte *dst = (byte *)screen.getBasePtr(destX, destY);
-	Graphics::keyBlit(dst, src, screen.pitch, bitmap.width, width, height,
-		screen.format.bytesPerPixel, kTransparentPaletteIndex);
-}
-
 static void drawShadowedString(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
 		int x, int y, int width, byte color, Graphics::TextAlign align = Graphics::kTextAlignLeft) {
 	font.drawString(&screen, text, x + 1, y + 1, width, kShadowColor, align);
diff --git a/engines/harvester/entity.cpp b/engines/harvester/entity.cpp
index 0a4a2e13de6..0cacae75e36 100644
--- a/engines/harvester/entity.cpp
+++ b/engines/harvester/entity.cpp
@@ -66,39 +66,8 @@ static void scaleIndexedBitmapNearest(const IndexedBitmap &source, IndexedBitmap
 
 static void blitAnimationFrame(Graphics::Screen &screen, const Common::Array<AbmFrame> &frames, uint frameIndex,
 		int x, int y) {
-	if (frameIndex >= frames.size() || !frames[frameIndex].isValid())
-		return;
-
-	const AbmFrame &frame = frames[frameIndex];
-	int destX = x;
-	int destY = y;
-	int srcX = 0;
-	int srcY = 0;
-	int width = (int)frame.width;
-	int height = (int)frame.height;
-
-	if (destX < 0) {
-		srcX = -destX;
-		width += destX;
-		destX = 0;
-	}
-	if (destY < 0) {
-		srcY = -destY;
-		height += destY;
-		destY = 0;
-	}
-	if (destX >= screen.w || destY >= screen.h || width <= 0 || height <= 0)
-		return;
-
-	width = MIN<int>(width, screen.w - destX);
-	height = MIN<int>(height, screen.h - destY);
-	if (width <= 0 || height <= 0)
-		return;
-
-	const byte *src = frame.pixels.data() + srcY * frame.width + srcX;
-	byte *dst = (byte *)screen.getBasePtr(destX, destY);
-	Graphics::keyBlit(dst, src, screen.pitch, frame.width, width, height,
-		screen.format.bytesPerPixel, kTransparentPaletteIndex);
+	if (frameIndex < frames.size())
+		blitTransparentBitmap(screen, frames[frameIndex], x, y);
 }
 
 } // End of anonymous namespace
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 3e40804f3d4..21bc79e07d5 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -32,7 +32,6 @@
 #include "common/ptr.h"
 #include "common/serializer.h"
 #include "common/system.h"
-#include "graphics/blit.h"
 #include "graphics/font.h"
 #include "graphics/fontman.h"
 #include "graphics/framelimiter.h"
@@ -586,26 +585,6 @@ static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, in
 	screen.copyRectToSurface(bitmap.pixels.data(), bitmap.width, x, y, bitmap.width, bitmap.height);
 }
 
-static void blitTransparentBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap,
-		int x, int y) {
-	if (!bitmap.isValid())
-		return;
-
-	for (uint32 srcY = 0; srcY < bitmap.height; ++srcY) {
-		const int destY = y + (int)srcY;
-		if (destY < 0 || destY >= screen.h)
-			continue;
-		for (uint32 srcX = 0; srcX < bitmap.width; ++srcX) {
-			const int destX = x + (int)srcX;
-			if (destX < 0 || destX >= screen.w)
-				continue;
-			const byte color = bitmap.pixels[srcY * bitmap.width + srcX];
-			if (color != 0)
-				*(byte *)screen.getBasePtr(destX, destY) = color;
-		}
-	}
-}
-
 bool captureScreenBackdrop(const Graphics::Screen &screen, IndexedBitmap &bitmap) {
 	if (screen.w <= 0 || screen.h <= 0 || screen.format.bytesPerPixel != 1)
 		return false;
diff --git a/engines/harvester/inventory.cpp b/engines/harvester/inventory.cpp
index 3054f5e7705..5670dc0a88c 100644
--- a/engines/harvester/inventory.cpp
+++ b/engines/harvester/inventory.cpp
@@ -23,7 +23,6 @@
 
 #include "common/algorithm.h"
 #include "common/debug.h"
-#include "graphics/blit.h"
 #include "graphics/font.h"
 #include "graphics/screen.h"
 #include "harvester/detection.h"
@@ -43,7 +42,6 @@ static const int kInventoryItemStartX = 73;
 static const int kInventoryItemStartY = 115;
 static const int kInventoryItemMaxRight = 564;
 static const int kInventoryItemSpacing = 5;
-static const byte kTransparentPaletteIndex = 0;
 static const char *const kHarvestBladeObjectName = "HARVEST_BLADE";
 
 struct InventoryCombatLoadoutEntry {
@@ -113,41 +111,6 @@ static const char *const kInventoryObjectActionItems[] = {
 	"ST_VITAMN"
 };
 
-static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
-	if (!bitmap.isValid())
-		return;
-
-	int destX = x;
-	int destY = y;
-	int srcX = 0;
-	int srcY = 0;
-	int width = (int)bitmap.width;
-	int height = (int)bitmap.height;
-
-	if (destX < 0) {
-		srcX = -destX;
-		width += destX;
-		destX = 0;
-	}
-	if (destY < 0) {
-		srcY = -destY;
-		height += destY;
-		destY = 0;
-	}
-	if (destX >= screen.w || destY >= screen.h || width <= 0 || height <= 0)
-		return;
-
-	width = MIN<int>(width, screen.w - destX);
-	height = MIN<int>(height, screen.h - destY);
-	if (width <= 0 || height <= 0)
-		return;
-
-	const byte *src = bitmap.pixels.data() + srcY * bitmap.width + srcX;
-	byte *dst = (byte *)screen.getBasePtr(destX, destY);
-	Graphics::keyBlit(dst, src, screen.pitch, bitmap.width, width, height,
-		screen.format.bytesPerPixel, kTransparentPaletteIndex);
-}
-
 static Common::Rect getHotspotBounds(const ObjectRecord &object) {
 	if (object.boundsX2 > object.currentX && object.boundsY2 > object.currentY)
 		return Common::Rect(object.currentX, object.currentY, object.boundsX2 + 1, object.boundsY2 + 1);
@@ -526,7 +489,7 @@ void InventorySystem::drawSelectedDragItem(Graphics::Screen &screen, const Commo
 
 		const int drawX = point.x - (int)item.bitmap.width / 2;
 		const int drawY = point.y - (int)item.bitmap.height / 2;
-		blitBitmap(screen, item.bitmap, drawX, drawY);
+		blitTransparentBitmap(screen, item.bitmap, drawX, drawY);
 		return;
 	}
 }
@@ -536,13 +499,13 @@ void InventorySystem::drawOverlay(Graphics::Screen &screen) const {
 	if (!art)
 		return;
 
-	blitBitmap(screen, art->getInventoryBitmap(), kInventoryX, kInventoryY);
+	blitTransparentBitmap(screen, art->getInventoryBitmap(), kInventoryX, kInventoryY);
 
 	for (const InventoryVisual &item : _items) {
 		if (!_selectedItemName.empty() && item.object.objectName.equalsIgnoreCase(_selectedItemName))
 			continue;
 		if (item.hasBitmap && item.bitmap.isValid())
-			blitBitmap(screen, item.bitmap, item.object.currentX, item.object.currentY);
+			blitTransparentBitmap(screen, item.bitmap, item.object.currentX, item.object.currentY);
 	}
 }
 
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 0a60adc48f4..01a98a7cfc4 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -28,7 +28,6 @@
 #include "common/memstream.h"
 #include "common/system.h"
 #include "engines/metaengine.h"
-#include "graphics/blit.h"
 #include "graphics/font.h"
 #include "graphics/fontman.h"
 #include "graphics/framelimiter.h"
@@ -60,7 +59,6 @@ static const byte kTextColorHover = 251;
 static const byte kTextColorDim = 248;
 static const byte kShadowColor = 0;
 static const byte kPanelFillColor = 1;
-static const byte kTransparentPaletteIndex = 0;
 static const byte kQuickTipActionColor = 0xc3;
 
 static const char *const kMenuPath = "MENU.INI";
@@ -527,41 +525,6 @@ static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, in
 	screen.copyRectToSurface(bitmap.pixels.data(), bitmap.width, x, y, bitmap.width, bitmap.height);
 }
 
-static void blitTransparentBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
-	if (!bitmap.isValid())
-		return;
-
-	int srcX = 0;
-	int srcY = 0;
-	int destX = x;
-	int destY = y;
-	int width = (int)bitmap.width;
-	int height = (int)bitmap.height;
-
-	if (destX < 0) {
-		srcX = -destX;
-		width += destX;
-		destX = 0;
-	}
-	if (destY < 0) {
-		srcY = -destY;
-		height += destY;
-		destY = 0;
-	}
-	if (destX >= screen.w || destY >= screen.h || width <= 0 || height <= 0)
-		return;
-
-	width = MIN<int>(width, screen.w - destX);
-	height = MIN<int>(height, screen.h - destY);
-	if (width <= 0 || height <= 0)
-		return;
-
-	const byte *src = bitmap.pixels.data() + srcY * bitmap.width + srcX;
-	byte *dst = (byte *)screen.getBasePtr(destX, destY);
-	Graphics::keyBlit(dst, src, screen.pitch, bitmap.width, width, height,
-		screen.format.bytesPerPixel, kTransparentPaletteIndex);
-}
-
 static void drawShadowedString(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
 		int x, int y, int width, byte color, Graphics::TextAlign align = Graphics::kTextAlignLeft) {
 	font.drawString(&screen, text, x + 1, y + 1, width, kShadowColor, align);
diff --git a/engines/harvester/npc/chessmaster_dialogue.cpp b/engines/harvester/npc/chessmaster_dialogue.cpp
index 0cebc88beb4..ad4a21c3002 100644
--- a/engines/harvester/npc/chessmaster_dialogue.cpp
+++ b/engines/harvester/npc/chessmaster_dialogue.cpp
@@ -24,7 +24,6 @@
 
 #include "common/events.h"
 #include "common/system.h"
-#include "graphics/blit.h"
 #include "graphics/framelimiter.h"
 #include "graphics/screen.h"
 #include "harvester/harvester.h"
@@ -40,7 +39,6 @@ static const char *const kChessmasterNpc = "CHESSMASTER";
 static const char *const kChessSolvedFlag = "CHESS_SOLVED";
 static const char *const kChessKingMonsterActionTag = "ADD_CHESS_KING_MONSTER";
 static const int kChessmasterSlashDamageType = 2;
-static const byte kTransparentPaletteIndex = 0;
 static const uint32 kChessmasterSuccessHoldMs = 1000;
 
 struct ChessmasterHotspot {
@@ -128,41 +126,6 @@ static bool pointHitsAnyHotspot(const Common::Point &point, const ChessmasterHot
 	return false;
 }
 
-static void blitTransparentFrame(Graphics::Screen &screen, const Graphics::Surface &frame, int x, int y) {
-	if (frame.w <= 0 || frame.h <= 0)
-		return;
-
-	int destX = x;
-	int destY = y;
-	int srcX = 0;
-	int srcY = 0;
-	int width = frame.w;
-	int height = frame.h;
-
-	if (destX < 0) {
-		srcX = -destX;
-		width += destX;
-		destX = 0;
-	}
-	if (destY < 0) {
-		srcY = -destY;
-		height += destY;
-		destY = 0;
-	}
-	if (destX >= screen.w || destY >= screen.h || width <= 0 || height <= 0)
-		return;
-
-	width = MIN<int>(width, screen.w - destX);
-	height = MIN<int>(height, screen.h - destY);
-	if (width <= 0 || height <= 0)
-		return;
-
-	const byte *src = (const byte *)frame.getBasePtr(srcX, srcY);
-	byte *dst = (byte *)screen.getBasePtr(destX, destY);
-	Graphics::keyBlit(dst, src, screen.pitch, frame.pitch, width, height,
-		screen.format.bytesPerPixel, kTransparentPaletteIndex);
-}
-
 static Common::Error decodeInitialFlicFrame(DialogueRuntime &runtime, const char *path,
 		ChessmasterFlicState &state) {
 	ResourceManager *resources = runtime.engine().getResources();
@@ -333,7 +296,7 @@ static Common::Error runChessmasterChoiceStage(DialogueRuntime &runtime,
 		drawFlicFrame(*screen, background);
 		const ChessmasterFlicState &cursorState = onInteractiveHotspot ? operateCursor : pointer;
 		if (cursorState.frame)
-			blitTransparentFrame(*screen, *cursorState.frame, mousePos.x, mousePos.y);
+			screen->transBlitFrom(*cursorState.frame, mousePos, 0);
 		screen->makeAllDirty();
 		screen->update();
 
diff --git a/engines/harvester/room_presentation.cpp b/engines/harvester/room_presentation.cpp
index 7cfc3505581..024c15148c6 100644
--- a/engines/harvester/room_presentation.cpp
+++ b/engines/harvester/room_presentation.cpp
@@ -26,7 +26,6 @@
 #include "common/events.h"
 #include "common/system.h"
 #include "common/textconsole.h"
-#include "graphics/blit.h"
 #include "graphics/framelimiter.h"
 #include "harvester/art.h"
 #include "harvester/detection.h"
@@ -38,7 +37,6 @@
 
 namespace Harvester {
 
-static const byte kTransparentPaletteIndex = 0;
 static const float kNormalPaletteBrightness = 1.0f;
 static const float kDimPaletteBrightness = 0.6f;
 
@@ -58,41 +56,6 @@ static void setScaledRoomPalette(Graphics::Screen &screen, const byte *palette,
 	screen.setPalette(scaledPalette);
 }
 
-void RoomPresentation::blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
-	if (!bitmap.isValid())
-		return;
-
-	int destX = x;
-	int destY = y;
-	int srcX = 0;
-	int srcY = 0;
-	int width = (int)bitmap.width;
-	int height = (int)bitmap.height;
-
-	if (destX < 0) {
-		srcX = -destX;
-		width += destX;
-		destX = 0;
-	}
-	if (destY < 0) {
-		srcY = -destY;
-		height += destY;
-		destY = 0;
-	}
-	if (destX >= screen.w || destY >= screen.h || width <= 0 || height <= 0)
-		return;
-
-	width = MIN<int>(width, screen.w - destX);
-	height = MIN<int>(height, screen.h - destY);
-	if (width <= 0 || height <= 0)
-		return;
-
-	const byte *src = bitmap.pixels.data() + srcY * bitmap.width + srcX;
-	byte *dst = (byte *)screen.getBasePtr(destX, destY);
-	Graphics::keyBlit(dst, src, screen.pitch, bitmap.width, width, height,
-		screen.format.bytesPerPixel, kTransparentPaletteIndex);
-}
-
 RoomPresentation::RoomPresentation(HarvesterEngine &engine, SceneState &scene,
 		const Art *art, const Graphics::Font *inspectFont, bool useNativeInspectFont)
 	: _engine(engine), _scene(scene), _art(art), _inspectFont(inspectFont),
@@ -208,7 +171,7 @@ Common::Error RoomPresentation::runModalShowText(const ResolvedText &modalText,
 		if (!initialInputReleased && frameCount++ > 0)
 			initialInputReleased = true;
 
-		blitBitmap(*activeScreen, backdrop, 0, 0);
+		blitTransparentBitmap(*activeScreen, backdrop, 0, 0);
 		setScaledRoomPalette(*activeScreen, _scene.palette, _scene.targetPaletteBrightness);
 		drawRoomInspectText(*activeScreen, *_art, *_inspectFont, modalText, _useNativeInspectFont);
 		activeScreen->makeAllDirty();
diff --git a/engines/harvester/room_presentation.h b/engines/harvester/room_presentation.h
index 93ecb8985f8..d5f39737f9d 100644
--- a/engines/harvester/room_presentation.h
+++ b/engines/harvester/room_presentation.h
@@ -66,7 +66,6 @@ public:
 	bool captureDialogueBackdrop(IndexedBitmap &dialogueBackdrop);
 	Common::Error applyLightingCommand(StartupLightingCommand lightingCommand);
 	Common::Error runModalShowText(const ResolvedText &modalText, Flow &flow);
-	static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y);
 
 private:
 	HarvesterEngine &_engine;
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index c66b32a234b..63a04825872 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -117,7 +117,7 @@ static void drawCombatLoadoutResourceIcons(Graphics::Screen &screen, const Art &
 
 	const int iconCount = CLIP<int>(script.getPlayerCombatResourceCount(loadout), 0, info.maxVisibleCount);
 	for (int i = 0; i < iconCount; ++i)
-		RoomPresentation::blitBitmap(screen, icon, kNativeCombatResourceIconsX + i * kNativeCombatResourceIconSpacing,
+		blitTransparentBitmap(screen, icon, kNativeCombatResourceIconsX + i * kNativeCombatResourceIconSpacing,
 			kNativeCombatResourceIconsY);
 }
 
@@ -562,7 +562,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 	} else if (roomCarryActive && _roomItems.getCarriedRoomItemBitmap().isValid()) {
 		const int drawX = _flow._mousePos.x - (int)_roomItems.getCarriedRoomItemBitmap().width / 2;
 		const int drawY = _flow._mousePos.y - (int)_roomItems.getCarriedRoomItemBitmap().height / 2;
-		RoomPresentation::blitBitmap(*activeScreen, _roomItems.getCarriedRoomItemBitmap(), drawX, drawY);
+		blitTransparentBitmap(*activeScreen, _roomItems.getCarriedRoomItemBitmap(), drawX, drawY);
 	}
 
 	if (_presentationState.showingInspectText) {


Commit: 19926d2016cd25cacd6e1837b8256b1c5104763d
    https://github.com/scummvm/scummvm/commit/19926d2016cd25cacd6e1837b8256b1c5104763d
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:06-04:00

Commit Message:
HARVESTER: add text editing helpers

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/dialogue.cpp
    engines/harvester/text.cpp
    engines/harvester/text.h


diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 4835f476fe5..80d29481e48 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -768,19 +768,17 @@ public:
 							cancelInput = true;
 						} else if (event.kbd.keycode == Common::KEYCODE_BACKSPACE ||
 								event.kbd.keycode == Common::KEYCODE_LEFT) {
-							if (!typedTopic.empty())
-								typedTopic.deleteLastChar();
+							deleteTextEntryCharacter(typedTopic);
 						} else if (event.kbd.keycode == Common::KEYCODE_HOME) {
 							typedTopic.clear();
 						} else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
 								event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
 							acceptInput = true;
 						} else if (event.kbd.ascii >= 0x1f && event.kbd.ascii <= 0x80 &&
-								event.kbd.ascii != '~' && typedTopic.size() < kDialogueTextEntryMaxLength &&
+								event.kbd.ascii != '~' &&
 								(!_subtitleCftFont || _subtitleCftFont->hasGlyph(event.kbd.ascii))) {
-							typedTopic += (char)event.kbd.ascii;
-							if (_highlightFont->getStringWidth(typedTopic) > kDialogueTextEntryMaxWidth)
-								typedTopic.deleteLastChar();
+							appendTextEntryCharacter(typedTopic, *_highlightFont, event.kbd.ascii,
+								kDialogueTextEntryMaxLength, kDialogueTextEntryMaxWidth);
 						}
 					} else if (event.type == Common::EVENT_LBUTTONDOWN) {
 						acceptInput = Common::Rect(3, 460, 318, 477).contains(event.mouse);
diff --git a/engines/harvester/text.cpp b/engines/harvester/text.cpp
index d2a61cc6754..dffc9693187 100644
--- a/engines/harvester/text.cpp
+++ b/engines/harvester/text.cpp
@@ -28,12 +28,33 @@
 #include "common/fs.h"
 #include "common/ptr.h"
 #include "common/stream.h"
+#include "graphics/font.h"
 #include "harvester/harvester.h"
 #include "harvester/parse_utils.h"
 #include "harvester/resources.h"
 
 namespace Harvester {
 
+bool appendTextEntryCharacter(Common::String &text, const Graphics::Font &font,
+		uint32 character, uint maxCharacters, int maxWidth) {
+	if (text.size() >= maxCharacters)
+		return false;
+
+	const Common::String candidate = text + (char)character;
+	if (font.getStringWidth(candidate) > maxWidth)
+		return false;
+
+	text = candidate;
+	return true;
+}
+
+bool deleteTextEntryCharacter(Common::String &text) {
+	if (text.empty())
+		return false;
+	text.deleteLastChar();
+	return true;
+}
+
 namespace {
 
 static const char *const kStartupFontPaths[] = {
diff --git a/engines/harvester/text.h b/engines/harvester/text.h
index c73d3b8086f..3d64d0879ba 100644
--- a/engines/harvester/text.h
+++ b/engines/harvester/text.h
@@ -25,10 +25,18 @@
 #include "common/array.h"
 #include "common/str.h"
 
+namespace Graphics {
+class Font;
+}
+
 namespace Harvester {
 
 class ResourceManager;
 
+bool appendTextEntryCharacter(Common::String &text, const Graphics::Font &font,
+	uint32 character, uint maxCharacters, int maxWidth);
+bool deleteTextEntryCharacter(Common::String &text);
+
 struct DialogueIndexEntry {
 	int wavId = 0;
 	uint32 textOffset = 0;


Commit: 41f2eba3b07420a459a68675f36499a00c959904
    https://github.com/scummvm/scummvm/commit/41f2eba3b07420a459a68675f36499a00c959904
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:06-04:00

Commit Message:
HARVESTER: refactor save/load to use a common slot system

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/menu.cpp
    engines/harvester/menu.h


diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 01a98a7cfc4..0e83333470b 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -497,23 +497,6 @@ static Common::String buildOptionsMenuItemLabel(const Script &script,
 	}
 }
 
-static bool appendBoundedTextEntryCharacter(Common::String &text, const Graphics::Font &font,
-		uint32 ascii, uint maxCharacters, int maxWidth) {
-	if (ascii < 32 || ascii > 126 || ascii == '~' || text.size() >= maxCharacters)
-		return false;
-
-	const Common::String candidate = text + (char)ascii;
-	if (font.getStringWidth(candidate) > maxWidth)
-		return false;
-
-	text = candidate;
-	return true;
-}
-
-static bool appendInlineSaveCharacter(Common::String &text, const Graphics::Font &font, uint32 ascii) {
-	return appendBoundedTextEntryCharacter(text, font, ascii, kSaveSlotMaxCharacters, kSaveSlotTextWidth);
-}
-
 static Common::String buildDefaultSaveSlotLabel(int slotIndex) {
 	return Common::String::format("GAME %02d", slotIndex + 1);
 }
@@ -656,43 +639,6 @@ static void renderQuickTipsOverlay(HarvesterEngine &engine, const IndexedBitmap
 	screen->update();
 }
 
-static void renderSaveGameMenuScreen(HarvesterEngine &engine, const IndexedBitmap &background,
-		const byte *palette, float paletteBrightness, const Graphics::Font &selectedLabelFont,
-		const Graphics::Font &unselectedLabelFont, const Graphics::Font &slotNameFont,
-		const Common::Array<Common::String> &slotTitles, int activeSlot,
-		const Common::String &statusMessage, int editingSlot = -1,
-		const Common::String *editingText = nullptr) {
-	Graphics::Screen *screen = engine.getScreen();
-	if (!screen)
-		return;
-
-	applyMenuPalette(*screen, engine, palette, paletteBrightness);
-	blitBitmap(*screen, background, 0, 0);
-
-	for (int i = 0; i < kSaveSlotCount; ++i) {
-		const Graphics::Font &labelFont = (i == activeSlot) ? selectedLabelFont : unselectedLabelFont;
-		const int y = kSaveSlotStartY + i * kSaveSlotStride;
-		const Common::String slotLabel = buildDefaultSaveSlotLabel(i);
-		labelFont.drawString(screen, slotLabel, kSaveSlotLabelX, y,
-			labelFont.getStringWidth(slotLabel), 0);
-
-		Common::String displayTitle = slotTitles[i];
-		if (i == editingSlot && editingText)
-			displayTitle = *editingText + "_";
-		slotNameFont.drawString(screen, displayTitle, kSaveSlotNameX, y, kSaveSlotTextWidth, 0);
-	}
-
-	if (!statusMessage.empty()) {
-		drawShadowedString(*screen, slotNameFont, statusMessage, 20, kSaveStatusTextY,
-			screen->w - 40, kTextColorNormal, Graphics::kTextAlignCenter);
-	}
-
-	if (engine.getRuntimeEntities())
-		engine.getRuntimeEntities()->drawCursor(*screen);
-	screen->makeAllDirty();
-	screen->update();
-}
-
 static void renderConfirmPromptScreen(HarvesterEngine &engine, const IndexedBitmap &backdrop,
 		const byte *palette, float paletteBrightness, const Graphics::Font &promptFont,
 		const Graphics::Font &yesFont, const Graphics::Font &noFont,
@@ -1363,86 +1309,97 @@ Common::Error MenuSystem::runRoomMenu(const IndexedBitmap &backdrop, const byte
 	return Common::kNoError;
 }
 
-Common::Error MenuSystem::runLoadGameMenu(const byte *palette, float paletteBrightness,
-		Flow &flow, bool &loadedGame) {
-	(void)palette;
-	(void)paletteBrightness;
-	loadedGame = false;
-
-	ResourceManager *resources = _engine.getResources();
-	MetaEngine *metaEngine = _engine.getMetaEngine();
-	const CftFontResource *slotNameFontResource = findStartupFontByName(_engine, "MEDFONT1");
-	const CftFontResource *slotLabelFontResource = findStartupFontByName(_engine, "MEDFONT2");
-	if (!resources || !metaEngine || !slotNameFontResource || !slotLabelFontResource)
-		return Common::kReadingFailed;
-	if (!_engine.canLoadGameStateCurrently())
-		return Common::kNoError;
-
-	HarvesterCftFont slotNameFont(*slotNameFontResource);
-	HarvesterCftFont slotLabelFont(*slotLabelFontResource);
-	if (!slotNameFont.isValid() || !slotLabelFont.isValid())
-		return Common::kReadingFailed;
+struct MenuSystem::SaveSlotMenu {
+	SaveSlotMenu(MetaEngine &meta, const CftFontResource &nameFont, const CftFontResource &labelFont)
+		: metaEngine(meta), slotNameFont(nameFont), slotLabelFont(labelFont) {}
 
+	MetaEngine &metaEngine;
+	HarvesterCftFont slotNameFont;
+	HarvesterCftFont slotLabelFont;
 	IndexedBitmap background;
-	byte loadPalette[256 * 3];
-	if (!loadBitmapResource(*resources, kLoadGameBitmapPath, background) ||
-			!loadPaletteResource(*resources, kLoadGamePalettePath, loadPalette)) {
-		return Common::kReadingFailed;
-	}
-
+	byte palette[256 * 3];
 	Common::Array<Common::String> slotTitles;
-	slotTitles.resize(kSaveSlotCount);
-	for (int i = 0; i < kSaveSlotCount; ++i)
-		slotTitles[i].clear();
+	int activeSlot = 0;
+	Common::String statusMessage;
+	bool needsRedraw = true;
+};
 
-	const SaveStateList saves = metaEngine->listSaves(ConfMan.getActiveDomainName().c_str());
+void MenuSystem::reloadSaveSlotTitles(SaveSlotMenu &menu) const {
+	menu.slotTitles.clear();
+	menu.slotTitles.resize(kSaveSlotCount);
+	const SaveStateList saves = menu.metaEngine.listSaves(ConfMan.getActiveDomainName().c_str());
 	for (const SaveStateDescriptor &save : saves) {
 		const int slot = save.getSaveSlot();
-		if (slot < 0 || slot >= kSaveSlotCount)
-			continue;
-		slotTitles[slot] = save.getDescription();
+		if (slot >= 0 && slot < kSaveSlotCount)
+			menu.slotTitles[slot] = save.getDescription();
 	}
+}
 
-	auto resolveHoveredSlot = [&]() -> int {
-		for (int slot = 0; slot < kSaveSlotCount; ++slot) {
-			if (saveSlotRect(slot).contains(_mousePos))
-				return slot;
-		}
-		return -1;
-	};
-
-	int activeSlot = 0;
+int MenuSystem::getSaveSlotAt(const Common::Point &mousePos) const {
 	for (int slot = 0; slot < kSaveSlotCount; ++slot) {
-		if (!slotTitles[slot].empty()) {
-			activeSlot = slot;
-			break;
-		}
+		if (saveSlotRect(slot).contains(mousePos))
+			return slot;
 	}
+	return -1;
+}
 
-	Common::String statusMessage;
-	bool needsRedraw = true;
-	flow.resetCursorAnimationSequence();
-	Graphics::FrameLimiter limiter(g_system, 60);
+MenuSystem::SaveSlotCommand MenuSystem::getSaveSlotCommand(const Common::Event &event) const {
+	if (event.type == Common::EVENT_KEYDOWN) {
+		if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+			return kCancelSlot;
+		if (event.kbd.keycode == Common::KEYCODE_RETURN || event.kbd.keycode == Common::KEYCODE_KP_ENTER)
+			return kConfirmSlot;
+	} else if (event.type == Common::EVENT_LBUTTONDOWN) {
+		if (saveConfirmRect().contains(_mousePos))
+			return kConfirmSlot;
+		if (saveCancelRect().contains(_mousePos))
+			return kCancelSlot;
+	}
+	return kNoSlotCommand;
+}
 
-	auto activateSelectedSlot = [&]() -> Common::Error {
-		if (slotTitles[activeSlot].empty())
-			return Common::kNoError;
+void MenuSystem::renderSaveSlotMenu(const SaveSlotMenu &menu, const Common::String *editingText) const {
+	Graphics::Screen *screen = _engine.getScreen();
+	if (!screen)
+		return;
 
-		Common::Error loadError = _engine.loadGameState(activeSlot);
-		if (loadError.getCode() == Common::kNoError) {
-			loadedGame = true;
-			return Common::kNoError;
-		}
+	applyMenuPalette(*screen, _engine, menu.palette, 1.0f);
+	blitBitmap(*screen, menu.background, 0, 0);
 
-		statusMessage = loadError.getDesc();
-		needsRedraw = true;
-		return Common::kNoError;
-	};
+	for (int i = 0; i < kSaveSlotCount; ++i) {
+		const Graphics::Font &labelFont = (i == menu.activeSlot) ? menu.slotNameFont : menu.slotLabelFont;
+		const int y = kSaveSlotStartY + i * kSaveSlotStride;
+		const Common::String slotLabel = buildDefaultSaveSlotLabel(i);
+		labelFont.drawString(screen, slotLabel, kSaveSlotLabelX, y,
+			labelFont.getStringWidth(slotLabel), 0);
+
+		Common::String displayTitle = menu.slotTitles[i];
+		if (i == menu.activeSlot && editingText)
+			displayTitle = *editingText + "_";
+		menu.slotNameFont.drawString(screen, displayTitle, kSaveSlotNameX, y, kSaveSlotTextWidth, 0);
+	}
+
+	if (!editingText && !menu.statusMessage.empty()) {
+		drawShadowedString(*screen, menu.slotNameFont, menu.statusMessage, 20, kSaveStatusTextY,
+			screen->w - 40, kTextColorNormal, Graphics::kTextAlignCenter);
+	}
+
+	if (_engine.getRuntimeEntities())
+		_engine.getRuntimeEntities()->drawCursor(*screen);
+	screen->makeAllDirty();
+	screen->update();
+}
+
+Common::Error MenuSystem::runSaveSlotEditor(SaveSlotMenu &menu, Flow &flow,
+		Common::String &editedTitle, bool &confirmed) {
+	confirmed = false;
+	bool needsRedraw = true;
+	flow.resetCursorAnimationSequence();
+	Graphics::FrameLimiter limiter(g_system, 60);
 
 	while (!_engine.shouldQuit()) {
 		if (needsRedraw) {
-			renderSaveGameMenuScreen(_engine, background, loadPalette, 1.0f,
-				slotNameFont, slotLabelFont, slotNameFont, slotTitles, activeSlot, statusMessage);
+			renderSaveSlotMenu(menu, &editedTitle);
 			needsRedraw = false;
 		}
 
@@ -1452,59 +1409,24 @@ Common::Error MenuSystem::runLoadGameMenu(const byte *palette, float paletteBrig
 			if (flow.handleSystemEvent(event, result))
 				return result;
 
+			const SaveSlotCommand command = getSaveSlotCommand(event);
+			if (command != kNoSlotCommand) {
+				confirmed = command == kConfirmSlot;
+				return Common::kNoError;
+			}
+
 			switch (event.type) {
-			case Common::EVENT_MOUSEMOVE: {
-				const int hoveredSlot = resolveHoveredSlot();
-				if (hoveredSlot != -1 && hoveredSlot != activeSlot)
-					activeSlot = hoveredSlot;
+			case Common::EVENT_MOUSEMOVE:
 				needsRedraw = true;
 				break;
-			}
-			case Common::EVENT_LBUTTONDOWN: {
-				const int hoveredSlot = resolveHoveredSlot();
-				statusMessage.clear();
-				if (hoveredSlot != -1) {
-					activeSlot = hoveredSlot;
-					Common::Error loadError = activateSelectedSlot();
-					if (loadError.getCode() != Common::kNoError)
-						return loadError;
-					if (loadedGame)
-						return Common::kNoError;
-					needsRedraw = true;
+			case Common::EVENT_KEYDOWN:
+				if (event.kbd.keycode == Common::KEYCODE_BACKSPACE) {
+					needsRedraw |= deleteTextEntryCharacter(editedTitle);
 					break;
 				}
-				if (saveConfirmRect().contains(_mousePos)) {
-					Common::Error loadError = activateSelectedSlot();
-					if (loadError.getCode() != Common::kNoError)
-						return loadError;
-					if (loadedGame)
-						return Common::kNoError;
-				} else if (saveCancelRect().contains(_mousePos)) {
-					return Common::kNoError;
-				}
-				needsRedraw = true;
-				break;
-			}
-			case Common::EVENT_KEYDOWN:
-				if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
-					return Common::kNoError;
-				if (event.kbd.keycode == Common::KEYCODE_UP) {
-					activeSlot = (activeSlot + kSaveSlotCount - 1) % kSaveSlotCount;
-					statusMessage.clear();
-					needsRedraw = true;
-				} else if (event.kbd.keycode == Common::KEYCODE_DOWN) {
-					activeSlot = (activeSlot + 1) % kSaveSlotCount;
-					statusMessage.clear();
-					needsRedraw = true;
-				} else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
-						event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
-					statusMessage.clear();
-					Common::Error loadError = activateSelectedSlot();
-					if (loadError.getCode() != Common::kNoError)
-						return loadError;
-					if (loadedGame)
-						return Common::kNoError;
-					needsRedraw = true;
+				if (event.kbd.ascii >= 32 && event.kbd.ascii <= 126 && event.kbd.ascii != '~') {
+					needsRedraw |= appendTextEntryCharacter(editedTitle, menu.slotNameFont,
+						event.kbd.ascii, kSaveSlotMaxCharacters, kSaveSlotTextWidth);
 				}
 				break;
 			default:
@@ -1522,154 +1444,89 @@ Common::Error MenuSystem::runLoadGameMenu(const byte *palette, float paletteBrig
 	return Common::kNoError;
 }
 
+void MenuSystem::loadSelectedSlot(SaveSlotMenu &menu, bool &loaded) {
+	if (menu.slotTitles[menu.activeSlot].empty())
+		return;
+
+	Common::Error loadError = _engine.loadGameState(menu.activeSlot);
+	if (loadError.getCode() == Common::kNoError) {
+		loaded = true;
+		return;
+	}
+
+	menu.statusMessage = loadError.getDesc();
+	menu.needsRedraw = true;
+}
+
+Common::Error MenuSystem::saveSelectedSlot(SaveSlotMenu &menu, Flow &flow, bool &saved) {
+	saved = false;
+	Common::String editedTitle = menu.slotTitles[menu.activeSlot];
+	bool confirmed = false;
+	Common::Error entryError = runSaveSlotEditor(menu, flow, editedTitle, confirmed);
+	if (entryError.getCode() != Common::kNoError)
+		return entryError;
+	if (!confirmed)
+		return Common::kNoError;
+
+	Common::Error saveError = _engine.saveGameState(menu.activeSlot, editedTitle);
+	if (saveError.getCode() == Common::kNoError) {
+		saved = true;
+		return Common::kNoError;
+	}
+
+	menu.statusMessage = saveError.getDesc();
+	reloadSaveSlotTitles(menu);
+	menu.needsRedraw = true;
+	return Common::kNoError;
+}
+
+Common::Error MenuSystem::runLoadGameMenu(const byte *palette, float paletteBrightness,
+		Flow &flow, bool &loadedGame) {
+	return runSaveSlotMenu(flow, kLoadMode, loadedGame);
+}
+
 Common::Error MenuSystem::runSaveGameMenu(const byte *palette, float paletteBrightness,
 		Flow &flow, bool &savedGame) {
-	(void)palette;
-	(void)paletteBrightness;
-	savedGame = false;
+	return runSaveSlotMenu(flow, kSaveMode, savedGame);
+}
+
+Common::Error MenuSystem::runSaveSlotMenu(Flow &flow, SaveLoadMode mode, bool &completed) {
+	completed = false;
 	ResourceManager *resources = _engine.getResources();
 	MetaEngine *metaEngine = _engine.getMetaEngine();
 	const CftFontResource *slotNameFontResource = findStartupFontByName(_engine, "MEDFONT1");
 	const CftFontResource *slotLabelFontResource = findStartupFontByName(_engine, "MEDFONT2");
 	if (!resources || !metaEngine || !slotNameFontResource || !slotLabelFontResource)
 		return Common::kReadingFailed;
-	if (!_engine.canSaveGameStateCurrently())
+	if (mode == kSaveMode ? !_engine.canSaveGameStateCurrently() : !_engine.canLoadGameStateCurrently())
 		return Common::kNoError;
 
-	HarvesterCftFont slotNameFont(*slotNameFontResource);
-	HarvesterCftFont slotLabelFont(*slotLabelFontResource);
-	if (!slotNameFont.isValid() || !slotLabelFont.isValid())
+	SaveSlotMenu menu(*metaEngine, *slotNameFontResource, *slotLabelFontResource);
+	if (!menu.slotNameFont.isValid() || !menu.slotLabelFont.isValid())
 		return Common::kReadingFailed;
 
-	IndexedBitmap background;
-	byte savePalette[256 * 3];
-	if (!loadBitmapResource(*resources, kSaveGameBitmapPath, background) ||
-			!loadPaletteResource(*resources, kSaveGamePalettePath, savePalette)) {
+	if (!loadBitmapResource(*resources, mode == kSaveMode ? kSaveGameBitmapPath : kLoadGameBitmapPath, menu.background) ||
+			!loadPaletteResource(*resources, mode == kSaveMode ? kSaveGamePalettePath : kLoadGamePalettePath, menu.palette)) {
 		return Common::kReadingFailed;
 	}
 
-	Common::Array<Common::String> slotTitles;
-	slotTitles.resize(kSaveSlotCount);
-	auto reloadSlotTitles = [&]() {
-		for (int i = 0; i < kSaveSlotCount; ++i)
-			slotTitles[i].clear();
-
-		const SaveStateList saves = metaEngine->listSaves(ConfMan.getActiveDomainName().c_str());
-		for (const SaveStateDescriptor &save : saves) {
-			const int slot = save.getSaveSlot();
-			if (slot < 0 || slot >= kSaveSlotCount)
-				continue;
-			slotTitles[slot] = save.getDescription();
-		}
-	};
-	reloadSlotTitles();
-
-	auto resolveHoveredSlot = [&]() -> int {
+	reloadSaveSlotTitles(menu);
+	if (mode == kLoadMode) {
 		for (int slot = 0; slot < kSaveSlotCount; ++slot) {
-			if (saveSlotRect(slot).contains(_mousePos))
-				return slot;
-		}
-		return -1;
-	};
-	auto runInlineSlotEditor = [&](int slotIndex, Common::String &editedTitle,
-			bool &confirmed) -> Common::Error {
-		confirmed = false;
-		bool needsRedraw = true;
-		flow.resetCursorAnimationSequence();
-		Graphics::FrameLimiter entryLimiter(g_system, 60);
-
-		while (!_engine.shouldQuit()) {
-			if (needsRedraw) {
-				renderSaveGameMenuScreen(_engine, background, savePalette, 1.0f,
-					slotNameFont, slotLabelFont, slotNameFont, slotTitles, slotIndex,
-					Common::String(), slotIndex, &editedTitle);
-				needsRedraw = false;
-			}
-
-			Common::Event event;
-			while (g_system->getEventManager()->pollEvent(event)) {
-				Common::Error result = Common::kNoError;
-				if (flow.handleSystemEvent(event, result))
-					return result;
-
-				switch (event.type) {
-				case Common::EVENT_MOUSEMOVE:
-					needsRedraw = true;
-					break;
-				case Common::EVENT_LBUTTONDOWN:
-					if (saveConfirmRect().contains(_mousePos)) {
-						confirmed = true;
-						return Common::kNoError;
-					}
-					if (saveCancelRect().contains(_mousePos))
-						return Common::kNoError;
-					break;
-				case Common::EVENT_KEYDOWN:
-					if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
-						return Common::kNoError;
-					if (event.kbd.keycode == Common::KEYCODE_RETURN ||
-							event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
-						confirmed = true;
-						return Common::kNoError;
-					}
-					if (event.kbd.keycode == Common::KEYCODE_BACKSPACE) {
-						if (!editedTitle.empty()) {
-							editedTitle.deleteLastChar();
-							needsRedraw = true;
-						}
-						break;
-					}
-					if (appendInlineSaveCharacter(editedTitle, slotNameFont, event.kbd.ascii))
-						needsRedraw = true;
-					break;
-				default:
-					break;
-				}
+			if (!menu.slotTitles[slot].empty()) {
+				menu.activeSlot = slot;
+				break;
 			}
-
-			if (EntityManager *entityManager = _engine.getRuntimeEntities())
-				(void)entityManager->syncCursorEntityPosition(_mousePos);
-
-			entryLimiter.delayBeforeSwap();
-			entryLimiter.startFrame();
 		}
+	}
 
-		return Common::kNoError;
-	};
-
-	int activeSlot = 0;
-	Common::String statusMessage;
-	bool needsRedraw = true;
 	flow.resetCursorAnimationSequence();
 	Graphics::FrameLimiter limiter(g_system, 60);
 
-	auto activateSelectedSlot = [&](bool &saved) -> Common::Error {
-		saved = false;
-		Common::String editedTitle = slotTitles[activeSlot];
-		bool confirmed = false;
-		Common::Error entryError = runInlineSlotEditor(activeSlot, editedTitle, confirmed);
-		if (entryError.getCode() != Common::kNoError)
-			return entryError;
-		if (!confirmed)
-			return Common::kNoError;
-
-		Common::Error saveError = _engine.saveGameState(activeSlot, editedTitle);
-		if (saveError.getCode() == Common::kNoError) {
-			saved = true;
-			return Common::kNoError;
-		}
-
-		statusMessage = saveError.getDesc();
-		reloadSlotTitles();
-		needsRedraw = true;
-		return Common::kNoError;
-	};
-
 	while (!_engine.shouldQuit()) {
-		if (needsRedraw) {
-			renderSaveGameMenuScreen(_engine, background, savePalette, 1.0f,
-				slotNameFont, slotLabelFont, slotNameFont, slotTitles, activeSlot, statusMessage);
-			needsRedraw = false;
+		if (menu.needsRedraw) {
+			renderSaveSlotMenu(menu);
+			menu.needsRedraw = false;
 		}
 
 		Common::Event event;
@@ -1678,71 +1535,64 @@ Common::Error MenuSystem::runSaveGameMenu(const byte *palette, float paletteBrig
 			if (flow.handleSystemEvent(event, result))
 				return result;
 
+			bool activateSlot = false;
+			bool redrawAfterActivation = false;
 			switch (event.type) {
 			case Common::EVENT_MOUSEMOVE: {
-				const int hoveredSlot = resolveHoveredSlot();
-				if (hoveredSlot != -1 && hoveredSlot != activeSlot)
-					activeSlot = hoveredSlot;
-				needsRedraw = true;
+				const int hoveredSlot = getSaveSlotAt(_mousePos);
+				if (hoveredSlot != -1 && hoveredSlot != menu.activeSlot)
+					menu.activeSlot = hoveredSlot;
+				menu.needsRedraw = true;
 				break;
 			}
 			case Common::EVENT_LBUTTONDOWN: {
-				const int hoveredSlot = resolveHoveredSlot();
-				bool saved = false;
-				statusMessage.clear();
+				const int hoveredSlot = getSaveSlotAt(_mousePos);
+				menu.statusMessage.clear();
 				if (hoveredSlot != -1) {
-					activeSlot = hoveredSlot;
-					Common::Error saveError = activateSelectedSlot(saved);
-					if (saveError.getCode() != Common::kNoError)
-						return saveError;
-					if (saved) {
-						savedGame = true;
-						return Common::kNoError;
-					}
-					break;
-				}
-				if (saveConfirmRect().contains(_mousePos)) {
-					Common::Error saveError = activateSelectedSlot(saved);
-					if (saveError.getCode() != Common::kNoError)
-						return saveError;
-					if (saved) {
-						savedGame = true;
+					menu.activeSlot = hoveredSlot;
+					activateSlot = true;
+					// Cancelling a row-opened save editor does not force a browser redraw.
+					redrawAfterActivation = mode == kLoadMode;
+				} else {
+					const SaveSlotCommand command = getSaveSlotCommand(event);
+					if (command == kCancelSlot)
 						return Common::kNoError;
-					}
-				} else if (saveCancelRect().contains(_mousePos)) {
-					return Common::kNoError;
+					activateSlot = command == kConfirmSlot;
+					redrawAfterActivation = true;
 				}
-				needsRedraw = true;
 				break;
 			}
 			case Common::EVENT_KEYDOWN:
-				if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+				if (getSaveSlotCommand(event) == kCancelSlot)
 					return Common::kNoError;
-				if (event.kbd.keycode == Common::KEYCODE_UP) {
-					activeSlot = (activeSlot + kSaveSlotCount - 1) % kSaveSlotCount;
-					statusMessage.clear();
-					needsRedraw = true;
-				} else if (event.kbd.keycode == Common::KEYCODE_DOWN) {
-					activeSlot = (activeSlot + 1) % kSaveSlotCount;
-					statusMessage.clear();
-					needsRedraw = true;
-				} else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
-						event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
-					bool saved = false;
-					statusMessage.clear();
-					Common::Error saveError = activateSelectedSlot(saved);
-					if (saveError.getCode() != Common::kNoError)
-						return saveError;
-					if (saved) {
-						savedGame = true;
-						return Common::kNoError;
-					}
-					needsRedraw = true;
+				if (event.kbd.keycode == Common::KEYCODE_UP || event.kbd.keycode == Common::KEYCODE_DOWN) {
+					const int step = event.kbd.keycode == Common::KEYCODE_UP ? kSaveSlotCount - 1 : 1;
+					menu.activeSlot = (menu.activeSlot + step) % kSaveSlotCount;
+					menu.statusMessage.clear();
+					menu.needsRedraw = true;
+				} else if (getSaveSlotCommand(event) == kConfirmSlot) {
+					menu.statusMessage.clear();
+					activateSlot = true;
+					redrawAfterActivation = true;
 				}
 				break;
 			default:
 				break;
 			}
+
+			if (activateSlot) {
+				Common::Error actionError = Common::kNoError;
+				if (mode == kSaveMode)
+					actionError = saveSelectedSlot(menu, flow, completed);
+				else
+					loadSelectedSlot(menu, completed);
+				if (actionError.getCode() != Common::kNoError)
+					return actionError;
+				if (completed)
+					return Common::kNoError;
+			}
+			if (redrawAfterActivation)
+				menu.needsRedraw = true;
 		}
 
 		if (EntityManager *entityManager = _engine.getRuntimeEntities())
@@ -1956,10 +1806,7 @@ Common::Error MenuSystem::runPasswordPrompt(const IndexedBitmap &backdrop, const
 				}
 				if (event.kbd.keycode == Common::KEYCODE_BACKSPACE ||
 						event.kbd.keycode == Common::KEYCODE_LEFT) {
-					if (!password.empty()) {
-						password.deleteLastChar();
-						needsRedraw = true;
-					}
+					needsRedraw |= deleteTextEntryCharacter(password);
 					break;
 				}
 				if (event.kbd.keycode == Common::KEYCODE_HOME) {
@@ -1969,9 +1816,9 @@ Common::Error MenuSystem::runPasswordPrompt(const IndexedBitmap &backdrop, const
 					}
 					break;
 				}
-				if (appendBoundedTextEntryCharacter(password, entryFont, event.kbd.ascii,
-						kPasswordMaxCharacters, kPasswordEntryWidth)) {
-					needsRedraw = true;
+				if (event.kbd.ascii >= 32 && event.kbd.ascii <= 126 && event.kbd.ascii != '~') {
+					needsRedraw |= appendTextEntryCharacter(password, entryFont, event.kbd.ascii,
+						kPasswordMaxCharacters, kPasswordEntryWidth);
 				}
 				break;
 			default:
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index 58dd3782ca9..b211ea9e6e3 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -32,6 +32,10 @@ namespace Graphics {
 class Font;
 }
 
+namespace Common {
+struct Event;
+}
+
 namespace Harvester {
 
 class HarvesterEngine;
@@ -96,6 +100,20 @@ public:
 		Flow &flow, bool &promptCompleted);
 
 private:
+	struct SaveSlotMenu;
+	enum SaveLoadMode { kSaveMode, kLoadMode };
+	enum SaveSlotCommand { kNoSlotCommand, kConfirmSlot, kCancelSlot };
+
+	Common::Error runSaveSlotMenu(Flow &flow, SaveLoadMode mode, bool &completed);
+	void reloadSaveSlotTitles(SaveSlotMenu &menu) const;
+	int getSaveSlotAt(const Common::Point &mousePos) const;
+	SaveSlotCommand getSaveSlotCommand(const Common::Event &event) const;
+	void renderSaveSlotMenu(const SaveSlotMenu &menu, const Common::String *editingText = nullptr) const;
+	Common::Error runSaveSlotEditor(SaveSlotMenu &menu, Flow &flow,
+		Common::String &editedTitle, bool &confirmed);
+	Common::Error saveSelectedSlot(SaveSlotMenu &menu, Flow &flow, bool &saved);
+	void loadSelectedSlot(SaveSlotMenu &menu, bool &loaded);
+
 	Common::Error runLoadGameMenu(const byte *palette, float paletteBrightness,
 		Flow &flow, bool &loadedGame);
 	Common::Error runOptionsMenu(const IndexedBitmap &backdrop, const byte *palette,


Commit: a8a11fd66c22d8fcb5af3487ccd8140f6b6da46a
    https://github.com/scummvm/scummvm/commit/a8a11fd66c22d8fcb5af3487ccd8140f6b6da46a
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:06-04:00

Commit Message:
HARVESTER: refactor NPC dialogue handling

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/dialogue.cpp
    engines/harvester/dialogue.h
    engines/harvester/npc/authority_dialogue.cpp
    engines/harvester/npc/authority_dialogue.h
    engines/harvester/npc/beggar_dialogue.cpp
    engines/harvester/npc/beggar_dialogue.h
    engines/harvester/npc/boyle_dialogue.cpp
    engines/harvester/npc/boyle_dialogue.h
    engines/harvester/npc/buster_dialogue.cpp
    engines/harvester/npc/buster_dialogue.h
    engines/harvester/npc/butcher_dialogue.cpp
    engines/harvester/npc/butcher_dialogue.h
    engines/harvester/npc/chessmaster_dialogue.cpp
    engines/harvester/npc/chessmaster_dialogue.h
    engines/harvester/npc/cloak_atnd_dialogue.cpp
    engines/harvester/npc/cloak_atnd_dialogue.h
    engines/harvester/npc/curator_dialogue.cpp
    engines/harvester/npc/curator_dialogue.h
    engines/harvester/npc/dad_dialogue.cpp
    engines/harvester/npc/dad_dialogue.h
    engines/harvester/npc/dark_woman_dialogue.cpp
    engines/harvester/npc/dark_woman_dialogue.h
    engines/harvester/npc/dialogue_handler.h
    engines/harvester/npc/dialogue_runtime.h
    engines/harvester/npc/dwayne_dialogue.cpp
    engines/harvester/npc/dwayne_dialogue.h
    engines/harvester/npc/edna_dialogue.cpp
    engines/harvester/npc/edna_dialogue.h
    engines/harvester/npc/fireman1_dialogue.cpp
    engines/harvester/npc/fireman1_dialogue.h
    engines/harvester/npc/fireman2_dialogue.cpp
    engines/harvester/npc/fireman2_dialogue.h
    engines/harvester/npc/gladiator_dialogue.cpp
    engines/harvester/npc/gladiator_dialogue.h
    engines/harvester/npc/hank_dialogue.cpp
    engines/harvester/npc/hank_dialogue.h
    engines/harvester/npc/herrill_dialogue.cpp
    engines/harvester/npc/herrill_dialogue.h
    engines/harvester/npc/inquisitor_dialogue.cpp
    engines/harvester/npc/inquisitor_dialogue.h
    engines/harvester/npc/jimmy_dialogue.cpp
    engines/harvester/npc/jimmy_dialogue.h
    engines/harvester/npc/johnson_dialogue.cpp
    engines/harvester/npc/johnson_dialogue.h
    engines/harvester/npc/karin_dialogue.cpp
    engines/harvester/npc/karin_dialogue.h
    engines/harvester/npc/librarian_dialogue.cpp
    engines/harvester/npc/librarian_dialogue.h
    engines/harvester/npc/lodge_chef_dialogue.cpp
    engines/harvester/npc/lodge_chef_dialogue.h
    engines/harvester/npc/loomis_dialogue.cpp
    engines/harvester/npc/loomis_dialogue.h
    engines/harvester/npc/madam_dialogue.cpp
    engines/harvester/npc/madam_dialogue.h
    engines/harvester/npc/maint_man_dialogue.cpp
    engines/harvester/npc/maint_man_dialogue.h
    engines/harvester/npc/mcknight_dialogue.cpp
    engines/harvester/npc/mcknight_dialogue.h
    engines/harvester/npc/memb_dir_dialogue.cpp
    engines/harvester/npc/memb_dir_dialogue.h
    engines/harvester/npc/mom_dialogue.cpp
    engines/harvester/npc/mom_dialogue.h
    engines/harvester/npc/mother_dialogue.cpp
    engines/harvester/npc/mother_dialogue.h
    engines/harvester/npc/moynahan_dialogue.cpp
    engines/harvester/npc/moynahan_dialogue.h
    engines/harvester/npc/mr_potts_dialogue.cpp
    engines/harvester/npc/mr_potts_dialogue.h
    engines/harvester/npc/mrs_potts_dialogue.cpp
    engines/harvester/npc/mrs_potts_dialogue.h
    engines/harvester/npc/nude_man_dialogue.cpp
    engines/harvester/npc/nude_man_dialogue.h
    engines/harvester/npc/parsons_dialogue.cpp
    engines/harvester/npc/parsons_dialogue.h
    engines/harvester/npc/pastorelli_dialogue.cpp
    engines/harvester/npc/pastorelli_dialogue.h
    engines/harvester/npc/phelps_dialogue.cpp
    engines/harvester/npc/phelps_dialogue.h
    engines/harvester/npc/priest_dialogue.cpp
    engines/harvester/npc/priest_dialogue.h
    engines/harvester/npc/pta_mom_dialogue.cpp
    engines/harvester/npc/pta_mom_dialogue.h
    engines/harvester/npc/ryder_dialogue.cpp
    engines/harvester/npc/ryder_dialogue.h
    engines/harvester/npc/sergeant_dialogue.cpp
    engines/harvester/npc/sergeant_dialogue.h
    engines/harvester/npc/sparky_dialogue.cpp
    engines/harvester/npc/sparky_dialogue.h
    engines/harvester/npc/stephanie_dialogue.cpp
    engines/harvester/npc/stephanie_dialogue.h
    engines/harvester/npc/stub_dialogue.cpp
    engines/harvester/npc/stub_dialogue.h
    engines/harvester/npc/swell_dialogue.cpp
    engines/harvester/npc/swell_dialogue.h
    engines/harvester/npc/valet_dialogue.cpp
    engines/harvester/npc/valet_dialogue.h
    engines/harvester/npc/vet_dialogue.cpp
    engines/harvester/npc/vet_dialogue.h
    engines/harvester/npc/wasp_woman_dialogue.cpp
    engines/harvester/npc/wasp_woman_dialogue.h
    engines/harvester/npc/whaley_dialogue.cpp
    engines/harvester/npc/whaley_dialogue.h


diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 80d29481e48..2626ea93933 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -1338,84 +1338,89 @@ DialogueSystem::DialogueSystem(HarvesterEngine &engine, Common::Point &mousePos)
 
 DialogueSystem::~DialogueSystem() {
 	for (uint i = 0; i < _npcHandlers.size(); ++i)
-		delete _npcHandlers[i];
+		delete _npcHandlers[i].handler;
 }
 
-bool DialogueSystem::hasRoomNpcHandler(const Common::String &npcName) const {
-	for (uint i = 0; i < _npcHandlers.size(); ++i) {
-		if (_npcHandlers[i]->matchesNpc(npcName))
-			return true;
+NpcDialogueHandler *DialogueSystem::findNpcHandler(const Common::String &npcName) const {
+	for (const NpcHandlerRegistration &registration : _npcHandlers) {
+		for (const char *name : registration.names) {
+			if (npcName.equalsIgnoreCase(name))
+				return registration.handler;
+		}
 	}
+	return nullptr;
+}
 
-	return false;
+bool DialogueSystem::hasRoomNpcHandler(const Common::String &npcName) const {
+	return findNpcHandler(npcName) != nullptr;
 }
 
 void DialogueSystem::registerNpcHandlers() {
-	_npcHandlers.push_back(new AuthorityDialogueHandler());
-	_npcHandlers.push_back(new BeggarDialogueHandler());
-	_npcHandlers.push_back(new BoyleDialogueHandler());
-	_npcHandlers.push_back(new BusterDialogueHandler());
-	_npcHandlers.push_back(new ButcherDialogueHandler());
-	_npcHandlers.push_back(new DwayneDialogueHandler());
-	_npcHandlers.push_back(new EdnaDialogueHandler());
-	_npcHandlers.push_back(new Fireman1DialogueHandler());
-	_npcHandlers.push_back(new Fireman2DialogueHandler());
-	_npcHandlers.push_back(new ChessmasterDialogueHandler());
-	_npcHandlers.push_back(new CloakAtndDialogueHandler());
-	_npcHandlers.push_back(new CuratorDialogueHandler());
-	_npcHandlers.push_back(new DadDialogueHandler());
-	_npcHandlers.push_back(new DarkWomanDialogueHandler());
-	_npcHandlers.push_back(new JimmyDialogueHandler());
-	_npcHandlers.push_back(new InquisitorDialogueHandler());
-	_npcHandlers.push_back(new KarinDialogueHandler());
-	_npcHandlers.push_back(new WaspWomanDialogueHandler());
-	_npcHandlers.push_back(new GladiatorDialogueHandler());
-	_npcHandlers.push_back(new LibrarianDialogueHandler());
-	_npcHandlers.push_back(new LoomisDialogueHandler());
-	_npcHandlers.push_back(new McknightDialogueHandler());
-	_npcHandlers.push_back(new MembDirDialogueHandler());
-	_npcHandlers.push_back(new MaintManDialogueHandler());
-	_npcHandlers.push_back(new MadamDialogueHandler());
-	_npcHandlers.push_back(new MomDialogueHandler());
-	_npcHandlers.push_back(new MotherDialogueHandler());
-	_npcHandlers.push_back(new MoynahanDialogueHandler());
-	_npcHandlers.push_back(new MrPottsDialogueHandler());
-	_npcHandlers.push_back(new MrsPottsDialogueHandler());
-	_npcHandlers.push_back(new LodgeChefDialogueHandler());
-	_npcHandlers.push_back(new NudeManDialogueHandler());
-	_npcHandlers.push_back(new ParsonsDialogueHandler());
-	_npcHandlers.push_back(new PastorelliDialogueHandler());
-	_npcHandlers.push_back(new PhelpsDialogueHandler());
-	_npcHandlers.push_back(new PriestDialogueHandler());
-	_npcHandlers.push_back(new RyderDialogueHandler());
-	_npcHandlers.push_back(new ValetDialogueHandler());
-	_npcHandlers.push_back(new VetDialogueHandler());
-	_npcHandlers.push_back(new SparkyDialogueHandler());
-	_npcHandlers.push_back(new StephanieDialogueHandler());
-	_npcHandlers.push_back(new SwellDialogueHandler());
-	_npcHandlers.push_back(new WhaleyDialogueHandler());
-	_npcHandlers.push_back(new HankDialogueHandler());
-	_npcHandlers.push_back(new HerrillDialogueHandler());
-	_npcHandlers.push_back(new JohnsonDialogueHandler());
-	_npcHandlers.push_back(new PtaMomDialogueHandler());
-
-	_npcHandlers.push_back(new SergeantDialogueHandler());
-	_npcHandlers.push_back(new StubNpcDialogueHandler("PTA_MOM"));
+	_npcHandlers.push_back({ new AuthorityDialogueHandler(), { "AUTHORITY", "AUTHORITY2" } });
+	_npcHandlers.push_back({ new BeggarDialogueHandler(), { "BEGGAR" } });
+	_npcHandlers.push_back({ new BoyleDialogueHandler(), { "BOYLE" } });
+	_npcHandlers.push_back({ new BusterDialogueHandler(), { "BUSTER" } });
+	_npcHandlers.push_back({ new ButcherDialogueHandler(), { "BUTCHER" } });
+	_npcHandlers.push_back({ new DwayneDialogueHandler(), { "DWAYNE", "DWAYNE_DNALFT", "DWAYNE_ST_BEDRM" } });
+	_npcHandlers.push_back({ new EdnaDialogueHandler(), { "EDNA" } });
+	_npcHandlers.push_back({ new Fireman1DialogueHandler(), { "FIREMAN1" } });
+	_npcHandlers.push_back({ new Fireman2DialogueHandler(), { "FIREMAN2" } });
+	_npcHandlers.push_back({ new ChessmasterDialogueHandler(), { "CHESSMASTER" } });
+	_npcHandlers.push_back({ new CloakAtndDialogueHandler(), { "CLOAK_ATND" } });
+	_npcHandlers.push_back({ new CuratorDialogueHandler(), { "CURATOR" } });
+	_npcHandlers.push_back({ new DadDialogueHandler(), { "DAD" } });
+	_npcHandlers.push_back({ new DarkWomanDialogueHandler(), { "DARK_WOMAN" } });
+	_npcHandlers.push_back({ new JimmyDialogueHandler(), { "JIMMY" } });
+	_npcHandlers.push_back({ new InquisitorDialogueHandler(), { "INQUISITOR" } });
+	_npcHandlers.push_back({ new KarinDialogueHandler(), { "KARIN", "KARIN_STANDING", "KARIN_CEM10", "KARIN_OFFICE" } });
+	_npcHandlers.push_back({ new WaspWomanDialogueHandler(), { "WASP_WOMAN" } });
+	_npcHandlers.push_back({ new GladiatorDialogueHandler(), { "GLADIATOR" } });
+	_npcHandlers.push_back({ new LibrarianDialogueHandler(), { "LIBRARIAN" } });
+	_npcHandlers.push_back({ new LoomisDialogueHandler(), { "LOOMIS" } });
+	_npcHandlers.push_back({ new McknightDialogueHandler(), { "MCKNIGHT" } });
+	_npcHandlers.push_back({ new MembDirDialogueHandler(), { "MEMB_DIR" } });
+	_npcHandlers.push_back({ new MaintManDialogueHandler(), { "MAINT_MAN" } });
+	_npcHandlers.push_back({ new MadamDialogueHandler(), { "MADAM" } });
+	_npcHandlers.push_back({ new MomDialogueHandler(), { "MOM" } });
+	_npcHandlers.push_back({ new MotherDialogueHandler(), { "MOTHER" } });
+	_npcHandlers.push_back({ new MoynahanDialogueHandler(), { "MOYNAHAN", "MOYNAHAN_MBLM" } });
+	_npcHandlers.push_back({ new MrPottsDialogueHandler(), { "MR_POTTS", "POTTS_FLESH", "MR_POTTS_CEM10", "MR_POTTS_HALL" } });
+	_npcHandlers.push_back({ new MrsPottsDialogueHandler(), { "MRS_POTTS", "MRS_POTTS_ST_BEDRM" } });
+	_npcHandlers.push_back({ new LodgeChefDialogueHandler(), { "LODGE_CHEF" } });
+	_npcHandlers.push_back({ new NudeManDialogueHandler(), { "NUDE_MAN" } });
+	_npcHandlers.push_back({ new ParsonsDialogueHandler(), { "PARSONS" } });
+	_npcHandlers.push_back({ new PastorelliDialogueHandler(), { "PASTORELLI" } });
+	_npcHandlers.push_back({ new PhelpsDialogueHandler(), { "PHELPS" } });
+	_npcHandlers.push_back({ new PriestDialogueHandler(), { "PRIEST" } });
+	_npcHandlers.push_back({ new RyderDialogueHandler(), { "RYDER", "RYDER_TV_OUT" } });
+	_npcHandlers.push_back({ new ValetDialogueHandler(), { "VALET" } });
+	_npcHandlers.push_back({ new VetDialogueHandler(), { "VET" } });
+	_npcHandlers.push_back({ new SparkyDialogueHandler(), { "SPARKY" } });
+	_npcHandlers.push_back({ new StephanieDialogueHandler(), { "STEPHANIE" } });
+	_npcHandlers.push_back({ new SwellDialogueHandler(), { "SWELL" } });
+	_npcHandlers.push_back({ new WhaleyDialogueHandler(), { "WHALEY" } });
+	_npcHandlers.push_back({ new HankDialogueHandler(), { "HANK" } });
+	_npcHandlers.push_back({ new HerrillDialogueHandler(), { "HERRILL", "HERRILL_LOG" } });
+	_npcHandlers.push_back({ new JohnsonDialogueHandler(), { "JOHNSON" } });
+	_npcHandlers.push_back({ new PtaMomDialogueHandler(), { "PTA_MOM1", "PTA_MOM2", "PTA_MOM3", "PTA_MOM4", "PTA_MOM5" } });
+
+	_npcHandlers.push_back({ new SergeantDialogueHandler(), { "SERGEANT" } });
+	_npcHandlers.push_back({ new StubNpcDialogueHandler("PTA_MOM"), { "PTA_MOM" } });
 }
 
 void DialogueSystem::resetRoomNpcDialogueState() {
 	_sharedDialogueState = DialogueSharedState();
 	for (uint i = 0; i < _npcHandlers.size(); ++i)
-		_npcHandlers[i]->resetState();
+		_npcHandlers[i].handler->resetState();
 }
 
 void DialogueSystem::syncRuntimeSaveState(Common::Serializer &s) {
 	syncDialogueSharedState(s, _sharedDialogueState);
 	for (uint i = 0; i < _npcHandlers.size(); ++i)
-		_npcHandlers[i]->syncState(s);
+		_npcHandlers[i].handler->syncState(s);
 	if (s.isLoading()) {
 		for (uint i = 0; i < _npcHandlers.size(); ++i)
-			_npcHandlers[i]->migrateSharedState(_sharedDialogueState);
+			_npcHandlers[i].handler->migrateSharedState(_sharedDialogueState);
 	}
 }
 
@@ -1427,7 +1432,8 @@ Common::Error DialogueSystem::runRoomNpcDialogue(const IndexedBitmap &backdrop,
 	Text *text = _engine.getText();
 	if (!script || !text || !backdrop.isValid())
 		return Common::kReadingFailed;
-	if (!hasRoomNpcHandler(npc.npcName)) {
+	NpcDialogueHandler *handler = findNpcHandler(npc.npcName);
+	if (!handler) {
 		debugC(1, kDebugDialogue,
 			"Harvester: blocked room NPC dialogue npc='%s' reason='no handler'",
 			npc.npcName.c_str());
@@ -1453,13 +1459,7 @@ Common::Error DialogueSystem::runRoomNpcDialogue(const IndexedBitmap &backdrop,
 	DialogueRuntime runtime(
 		_engine, *script, *text, flow, currentRoomName, session.getGenericByeTopic(), session);
 
-	for (uint i = 0; i < _npcHandlers.size(); ++i) {
-		if (_npcHandlers[i]->matchesNpc(npc.npcName))
-			return _npcHandlers[i]->handleDialogue(runtime, usedItemName, _sharedDialogueState);
-	}
-
-	debug(1, "Harvester: unsupported room NPC dialogue handler '%s'", npc.npcName.c_str());
-	return Common::kNoError;
+	return handler->handleDialogue(runtime, usedItemName, _sharedDialogueState);
 }
 
 } // End of namespace Harvester
diff --git a/engines/harvester/dialogue.h b/engines/harvester/dialogue.h
index 4fe59c95ea8..d006caedf21 100644
--- a/engines/harvester/dialogue.h
+++ b/engines/harvester/dialogue.h
@@ -55,12 +55,18 @@ public:
 		Flow &flow);
 
 private:
+	struct NpcHandlerRegistration {
+		NpcDialogueHandler *handler;
+		Common::Array<const char *> names;
+	};
+
 	void registerNpcHandlers();
+	NpcDialogueHandler *findNpcHandler(const Common::String &npcName) const;
 
 	HarvesterEngine &_engine;
 	Common::Point &_mousePos;
 	DialogueSharedState _sharedDialogueState;
-	Common::Array<NpcDialogueHandler *> _npcHandlers;
+	Common::Array<NpcHandlerRegistration> _npcHandlers;
 };
 
 } // End of namespace Harvester
diff --git a/engines/harvester/npc/authority_dialogue.cpp b/engines/harvester/npc/authority_dialogue.cpp
index 075dd31aaa0..089a313b85c 100644
--- a/engines/harvester/npc/authority_dialogue.cpp
+++ b/engines/harvester/npc/authority_dialogue.cpp
@@ -26,30 +26,18 @@
 
 namespace Harvester {
 
-namespace {
-
-static const char *const kAuthorityNpc = "AUTHORITY";
-static const char *const kAuthority2Npc = "AUTHORITY2";
-
-} // End of namespace
-
-bool AuthorityDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kAuthorityNpc) ||
-		npcName.equalsIgnoreCase(kAuthority2Npc);
-}
-
 Common::Error AuthorityDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
 	if (runtime.startupScript().getFlagValue("STOP_AUTHOR_TALK"))
 		return Common::kNoError;
 
 	if (runtime.startupScript().getFlagValue("IF_DON_T_EAT_THE_FOOD")) {
-		Common::Error lineError = runtime.playDialogueLine(0x3992, kAuthorityNpc);
+		Common::Error lineError = runtime.playDialogueLine(0x3992, "AUTHORITY");
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
 		if (runtime.startupScript().getFlagValue("IF_YOU_EAT_THE_FOOD_AND_DEFEAT_THE_ENEMIES"))
-			return runtime.playDialogueLine(0x399b, kAuthorityNpc);
+			return runtime.playDialogueLine(0x399b, "AUTHORITY");
 
 		return Common::kNoError;
 	}
@@ -58,7 +46,7 @@ Common::Error AuthorityDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		runtime.startupScript().getFlagValue("IF_YOU_EAT_THE_FOOD_AND_DEFEAT_THE_ENEMIES")
 			? 0x399b
 			: 0x398a,
-		kAuthorityNpc);
+		"AUTHORITY");
 }
 
 } // End of namespace Harvester
diff --git a/engines/harvester/npc/authority_dialogue.h b/engines/harvester/npc/authority_dialogue.h
index 771441ef3f1..b123e73a7c7 100644
--- a/engines/harvester/npc/authority_dialogue.h
+++ b/engines/harvester/npc/authority_dialogue.h
@@ -28,7 +28,6 @@ namespace Harvester {
 
 class AuthorityDialogueHandler : public NpcDialogueHandler {
 public:
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {}
 	Common::Error handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) override;
diff --git a/engines/harvester/npc/beggar_dialogue.cpp b/engines/harvester/npc/beggar_dialogue.cpp
index 05f7de92c01..4276623c57f 100644
--- a/engines/harvester/npc/beggar_dialogue.cpp
+++ b/engines/harvester/npc/beggar_dialogue.cpp
@@ -28,8 +28,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kBeggarNpc = "BEGGAR";
-static const char *const kPcSpeaker = "PC";
 static const char *const kBeggarInterruptFlag = "BEGGAR_INTERRUPT_CONVERSATION";
 static const char *const kBeggarDialog3ActionTag = "BEGGAR_DIALOG_3";
 static const int kBeggarInitialResponseLine = 0x0;
@@ -37,21 +35,11 @@ static const int kBeggarFollowupResponseLine = 0x1;
 
 } // End of namespace
 
-bool BeggarDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kBeggarNpc);
-}
-
 Common::Error BeggarDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
-	auto playBeggarLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kBeggarNpc, headVariant);
-	};
-	auto playPcLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kPcSpeaker, headVariant);
-	};
 	auto queueBeggarMonsterfyTransition = [&]() {
 		InteractionResult interaction;
-		if (runtime.startupScript().queueRuntimeNpcDeathOrMonsterfy(kBeggarNpc)) {
+		if (runtime.startupScript().queueRuntimeNpcDeathOrMonsterfy("BEGGAR")) {
 			interaction.mutatedRuntimeState = true;
 			interaction.visualRuntimeStateChanged = true;
 		}
@@ -67,15 +55,15 @@ Common::Error BeggarDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		runtime.queueDialogueInteractionIfNeeded(interaction);
 	}
 
-	Common::Error lineError = playBeggarLine(0x1276, 1);
+	Common::Error lineError = runtime.playDialogueLineWithVariant(0x1276, "BEGGAR", 1);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playPcLine(0x127a, 2);
+	lineError = runtime.playDialogueLineWithVariant(0x127a, "PC", 2);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playBeggarLine(0x127e);
+	lineError = runtime.playDialogueLineWithVariant(0x127e, "BEGGAR", 0);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -86,9 +74,9 @@ Common::Error BeggarDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return responseError;
 
 	if (responseIndex == 1) {
-		lineError = playBeggarLine(0x128d, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x128d, "BEGGAR", 1);
 	} else if (responseIndex == 2) {
-		lineError = playBeggarLine(0x1291, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x1291, "BEGGAR", 3);
 	} else {
 		queueBeggarMonsterfyTransition();
 		return Common::kNoError;
@@ -96,7 +84,7 @@ Common::Error BeggarDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playBeggarLine(0x1295);
+	lineError = runtime.playDialogueLineWithVariant(0x1295, "BEGGAR", 0);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -107,11 +95,11 @@ Common::Error BeggarDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return responseError;
 
 	if (responseIndex == 1) {
-		lineError = playBeggarLine(0x12ab, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x12ab, "BEGGAR", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	} else if (responseIndex == 2) {
-		lineError = playBeggarLine(0x12a5, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x12a5, "BEGGAR", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
diff --git a/engines/harvester/npc/beggar_dialogue.h b/engines/harvester/npc/beggar_dialogue.h
index d16aad1e47b..8653f159c83 100644
--- a/engines/harvester/npc/beggar_dialogue.h
+++ b/engines/harvester/npc/beggar_dialogue.h
@@ -28,7 +28,6 @@ namespace Harvester {
 
 class BeggarDialogueHandler : public NpcDialogueHandler {
 public:
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {}
 	Common::Error handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) override;
diff --git a/engines/harvester/npc/boyle_dialogue.cpp b/engines/harvester/npc/boyle_dialogue.cpp
index 5f6946ad439..d146d94e034 100644
--- a/engines/harvester/npc/boyle_dialogue.cpp
+++ b/engines/harvester/npc/boyle_dialogue.cpp
@@ -29,8 +29,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kBoyleNpc = "BOYLE";
-static const char *const kPcSpeaker = "PC";
 static const char *const kInventoryOwnerName = "INVENTORY";
 static const char *const kBoylesButtonObject = "BOYLES_BUTTON";
 static const char *const kGascanObject = "GASCAN";
@@ -41,88 +39,88 @@ static const char *const kBoyleArsonistTopic = "Arsonist";
 static const char *const kDialogueC019BFstPath = "GRAPHIC/FST/C019B.FST";
 
 static const DialogueLineEntry kBoyleHaveLodgeAppEvidenceLines[] = {
-	{ 0x13d, kBoyleNpc, 2 },
-	{ 0x141, kPcSpeaker, 0 },
-	{ 0x145, kBoyleNpc, 2 },
-	{ 0x14a, kPcSpeaker, 0 },
-	{ 0x14e, kBoyleNpc, 3 }
+	{ 0x13d, "BOYLE", 2 },
+	{ 0x141, "PC", 0 },
+	{ 0x145, "BOYLE", 2 },
+	{ 0x14a, "PC", 0 },
+	{ 0x14e, "BOYLE", 3 }
 };
 
 static const DialogueLineEntry kBoyleButtonIntroLines[] = {
-	{ 0x155, kBoyleNpc, 1 },
-	{ 0x15a, kPcSpeaker, 0 },
-	{ 0x15f, kBoyleNpc, 2 }
+	{ 0x155, "BOYLE", 1 },
+	{ 0x15a, "PC", 0 },
+	{ 0x15f, "BOYLE", 2 }
 };
 
 static const DialogueLineEntry kBoyleButtonResponse2Lines[] = {
-	{ 0x174, kBoyleNpc, 0 },
-	{ 0x179, kPcSpeaker, 0 },
-	{ 0x17d, kBoyleNpc, 0 }
+	{ 0x174, "BOYLE", 0 },
+	{ 0x179, "PC", 0 },
+	{ 0x17d, "BOYLE", 0 }
 };
 
 static const DialogueLineEntry kBoyleNoteResponse1IntroLines[] = {
-	{ 0x1a1, kBoyleNpc, 2 },
-	{ 0x1a9, kPcSpeaker, 0 },
-	{ 0x1ad, kBoyleNpc, 0 }
+	{ 0x1a1, "BOYLE", 2 },
+	{ 0x1a9, "PC", 0 },
+	{ 0x1ad, "BOYLE", 0 }
 };
 
 static const DialogueLineEntry kBoyleGascanRewardLines[] = {
-	{ 0x1f2, kBoyleNpc, 3 },
-	{ 0x1f6, kPcSpeaker, 2 },
-	{ 0x1fa, kBoyleNpc, 3 },
-	{ 0x202, kPcSpeaker, 2 },
-	{ 0x206, kBoyleNpc, 2 }
+	{ 0x1f2, "BOYLE", 3 },
+	{ 0x1f6, "PC", 2 },
+	{ 0x1fa, "BOYLE", 3 },
+	{ 0x202, "PC", 2 },
+	{ 0x206, "BOYLE", 2 }
 };
 
 static const DialogueLineEntry kBoyleIntroOpeningLines[] = {
-	{ 0x7, kBoyleNpc, 0 },
-	{ 0xb, kPcSpeaker, 0 },
-	{ 0xf, kBoyleNpc, 1 }
+	{ 0x7, "BOYLE", 0 },
+	{ 0xb, "PC", 0 },
+	{ 0xf, "BOYLE", 1 }
 };
 
 static const DialogueLineEntry kBoyleIntroResponse1Lines[] = {
-	{ 0x19, kBoyleNpc, 0 },
-	{ 0x1d, kPcSpeaker, 2 },
-	{ 0x21, kBoyleNpc, 1 }
+	{ 0x19, "BOYLE", 0 },
+	{ 0x1d, "PC", 2 },
+	{ 0x21, "BOYLE", 1 }
 };
 
 static const DialogueLineEntry kBoyleIntroResponse1TailLines[] = {
-	{ 0x3a, kBoyleNpc, 0 },
-	{ 0x41, kPcSpeaker, 2 },
-	{ 0x45, kBoyleNpc, 0 }
+	{ 0x3a, "BOYLE", 0 },
+	{ 0x41, "PC", 2 },
+	{ 0x45, "BOYLE", 0 }
 };
 
 static const DialogueLineEntry kBoyleIntroResponse2Choice2Lines[] = {
-	{ 0x76, kBoyleNpc, 0 },
-	{ 0x77, kBoyleNpc, 2 }
+	{ 0x76, "BOYLE", 0 },
+	{ 0x77, "BOYLE", 2 }
 };
 
 static const DialogueLineEntry kBoyleKeywordTopic0eLines[] = {
-	{ 0x8c, kPcSpeaker, 0 },
-	{ 0x90, kBoyleNpc, 0 },
-	{ 0x94, kPcSpeaker, 0 },
-	{ 0x98, kBoyleNpc, 0 },
-	{ 0x9d, kPcSpeaker, 0 },
-	{ 0xa1, kBoyleNpc, 1 }
+	{ 0x8c, "PC", 0 },
+	{ 0x90, "BOYLE", 0 },
+	{ 0x94, "PC", 0 },
+	{ 0x98, "BOYLE", 0 },
+	{ 0x9d, "PC", 0 },
+	{ 0xa1, "BOYLE", 1 }
 };
 
 static const DialogueLineEntry kBoyleKeywordTopic17Lines[] = {
-	{ 0xee, kPcSpeaker, 0 },
-	{ 0xf2, kBoyleNpc, 2 },
-	{ 0xf6, kPcSpeaker, 4 },
-	{ 0xfa, kBoyleNpc, 0 }
+	{ 0xee, "PC", 0 },
+	{ 0xf2, "BOYLE", 2 },
+	{ 0xf6, "PC", 4 },
+	{ 0xfa, "BOYLE", 0 }
 };
 
 static const DialogueLineEntry kBoyleKeywordArsonResponse1Lines[] = {
-	{ 0x126, kBoyleNpc, 0 },
-	{ 0x12c, kPcSpeaker, 0 },
-	{ 0x130, kBoyleNpc, 2 }
+	{ 0x126, "BOYLE", 0 },
+	{ 0x12c, "PC", 0 },
+	{ 0x130, "BOYLE", 2 }
 };
 
 static const DialogueLineEntry kBoyleFollowupBurnedTvLines[] = {
-	{ 0x28a, kBoyleNpc, 1 },
-	{ 0x28e, kPcSpeaker, 0 },
-	{ 0x292, kBoyleNpc, 1 }
+	{ 0x28a, "BOYLE", 1 },
+	{ 0x28e, "PC", 0 },
+	{ 0x292, "BOYLE", 1 }
 };
 
 static const int kBoyleKeywordTopic0fTo12Lines[] = { 0xf, 0x10, 0x11, 0x12 };
@@ -132,33 +130,14 @@ static const int kBoyleKeywordTopic18To1bLines[] = { 0x18, 0x19, 0x1a, 0x1b };
 
 } // End of namespace
 
-bool BoyleDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kBoyleNpc);
-}
-
 Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	BoyleRoomDialogueState &state = _state;
 	Common::String boyleTopicBuffer;
 	int boyleTopicBufferLineIndex = -1;
 
-	auto assignBoyleTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(boyleTopicBuffer, boyleTopicBufferLineIndex,
-			responseLineIndex, "Boyle topic buffer");
-	};
-	auto clearBoyleTopicBuffer = [&]() {
-		boyleTopicBuffer.clear();
-		boyleTopicBufferLineIndex = -1;
-	};
-	auto playLine = [&](int wavId, const char *speakerId, int headVariant) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, speakerId, headVariant);
-	};
-	auto playBoyleLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return playLine(wavId, kBoyleNpc, headVariant);
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
+	DialogueTopicBuffer topics(runtime, boyleTopicBuffer, boyleTopicBufferLineIndex, "Boyle topic buffer");
+
 	auto hideInventoryItem = [&](const char *objectName) {
 		return runtime.startupScript().setRuntimeObjectVisible(
 			kInventoryOwnerName, objectName, false);
@@ -184,7 +163,7 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	if (!usedItemName.empty()) {
 		if (usedItemName.equalsIgnoreCase(kBoylesButtonObject)) {
-			lineError = playSequence(kBoyleButtonIntroLines, ARRAYSIZE(kBoyleButtonIntroLines));
+			lineError = runtime.playDialogueEntrySequence(kBoyleButtonIntroLines, ARRAYSIZE(kBoyleButtonIntroLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -194,26 +173,25 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playBoyleLine(0x16d, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x16d, "BOYLE", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				return playBoyleLine(0x100, 1);
+				return runtime.playDialogueLineWithVariant(0x100, "BOYLE", 1);
 			}
 			if (responseIndex == 2) {
-				lineError = playSequence(kBoyleButtonResponse2Lines,
-					ARRAYSIZE(kBoyleButtonResponse2Lines));
+				lineError = runtime.playDialogueEntrySequence(kBoyleButtonResponse2Lines, ARRAYSIZE(kBoyleButtonResponse2Lines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
 				if (state.extraEvidenceReplyEnabled) {
-					lineError = playLine(0x186, kPcSpeaker, 0);
+					lineError = runtime.playDialogueLineWithVariant(0x186, "PC", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
-					lineError = playBoyleLine(0x18b);
+					lineError = runtime.playDialogueLineWithVariant(0x18b, "BOYLE", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				} else {
-					lineError = playBoyleLine(0x1d2);
+					lineError = runtime.playDialogueLineWithVariant(0x1d2, "BOYLE", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				}
@@ -229,11 +207,11 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (isBlackmailEvidence(usedItemName)) {
 			if (runtime.startupScript().getFlagValue(kHaveLodgeAppFlag)) {
-				return playSequence(kBoyleHaveLodgeAppEvidenceLines,
+				return runtime.playDialogueEntrySequence(kBoyleHaveLodgeAppEvidenceLines,
 					ARRAYSIZE(kBoyleHaveLodgeAppEvidenceLines));
 			}
 
-			lineError = playBoyleLine(0x197);
+			lineError = runtime.playDialogueLineWithVariant(0x197, "BOYLE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -244,22 +222,22 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 			if (responseIndex == 1) {
 				sharedState.boyleGascanApplicationState = true;
-				lineError = playSequence(kBoyleNoteResponse1IntroLines,
+				lineError = runtime.playDialogueEntrySequence(kBoyleNoteResponse1IntroLines,
 					ARRAYSIZE(kBoyleNoteResponse1IntroLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				if (state.extraEvidenceReplyEnabled) {
-					lineError = playLine(0x186, kPcSpeaker, 0);
+					lineError = runtime.playDialogueLineWithVariant(0x186, "PC", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
-					lineError = playBoyleLine(0x1b8);
+					lineError = runtime.playDialogueLineWithVariant(0x1b8, "BOYLE", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				}
-				return playBoyleLine(0x1d2);
+				return runtime.playDialogueLineWithVariant(0x1d2, "BOYLE", 0);
 			}
 			if (responseIndex == 2) {
-				lineError = playBoyleLine(0x1d9, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x1d9, "BOYLE", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -269,17 +247,16 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 					return responseError;
 
 				if (followupResponseIndex == 1)
-					return playBoyleLine(0x1e5, 1);
+					return runtime.playDialogueLineWithVariant(0x1e5, "BOYLE", 1);
 				if (followupResponseIndex == 2)
-					return playBoyleLine(0x1ea, 2);
+					return runtime.playDialogueLineWithVariant(0x1ea, "BOYLE", 2);
 			}
 			return Common::kNoError;
 		}
 
 		if (usedItemName.equalsIgnoreCase(kGascanObject) &&
 				sharedState.boyleGascanApplicationState) {
-			lineError = playSequence(kBoyleGascanRewardLines,
-				ARRAYSIZE(kBoyleGascanRewardLines));
+			lineError = runtime.playDialogueEntrySequence(kBoyleGascanRewardLines, ARRAYSIZE(kBoyleGascanRewardLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -291,30 +268,30 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
 			sharedState.discussedWhaleyHerrillPhoto = 1;
-			return playBoyleLine(0x25f, 2);
+			return runtime.playDialogueLineWithVariant(0x25f, "BOYLE", 2);
 		}
 
 		if (usedItemName.equalsIgnoreCase("CASKET_PHOTO") ||
 				usedItemName.equalsIgnoreCase("CASKET_PHOTOCOPY")) {
 			sharedState.discussedCasketPhotoEvidence = 1;
-			lineError = playBoyleLine(0x24f, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x24f, "BOYLE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			if (sharedState.dialogueStateD2ebc) {
-				lineError = playLine(0x253, kPcSpeaker, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x253, "PC", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
-			return playBoyleLine(0x258, 2);
+			return runtime.playDialogueLineWithVariant(0x258, "BOYLE", 2);
 		}
 
-		return playBoyleLine(0x249);
+		return runtime.playDialogueLineWithVariant(0x249, "BOYLE", 0);
 	}
 
 	if (state.introPending) {
 		state.introPending = false;
 
-		lineError = playSequence(kBoyleIntroOpeningLines, ARRAYSIZE(kBoyleIntroOpeningLines));
+		lineError = runtime.playDialogueEntrySequence(kBoyleIntroOpeningLines, ARRAYSIZE(kBoyleIntroOpeningLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -325,8 +302,7 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		bool seedButtonTopicBuffer = false;
 		if (responseIndex == 1) {
-			lineError = playSequence(kBoyleIntroResponse1Lines,
-				ARRAYSIZE(kBoyleIntroResponse1Lines));
+			lineError = runtime.playDialogueEntrySequence(kBoyleIntroResponse1Lines, ARRAYSIZE(kBoyleIntroResponse1Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -336,22 +312,22 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (followupResponseIndex == 1) {
-				lineError = playBoyleLine(0x2f);
+				lineError = runtime.playDialogueLineWithVariant(0x2f, "BOYLE", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else if (followupResponseIndex == 2) {
-				lineError = playBoyleLine(0x34, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x34, "BOYLE", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 
-			lineError = playSequence(kBoyleIntroResponse1TailLines,
+			lineError = runtime.playDialogueEntrySequence(kBoyleIntroResponse1TailLines,
 				ARRAYSIZE(kBoyleIntroResponse1TailLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			seedButtonTopicBuffer = true;
 		} else if (responseIndex == 2) {
-			lineError = playBoyleLine(0x4d, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x4d, "BOYLE", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -361,7 +337,7 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (followupResponseIndex == 1) {
-				lineError = playBoyleLine(0x59, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x59, "BOYLE", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -371,59 +347,59 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 					return responseError;
 
 				if (nestedResponseIndex == 1) {
-					lineError = playBoyleLine(0x68, 2);
+					lineError = runtime.playDialogueLineWithVariant(0x68, "BOYLE", 2);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				} else if (nestedResponseIndex == 2) {
-					lineError = playBoyleLine(0x6c, 2);
+					lineError = runtime.playDialogueLineWithVariant(0x6c, "BOYLE", 2);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				}
 			} else if (followupResponseIndex == 2) {
 				state.extraEvidenceReplyEnabled = true;
-				lineError = playSequence(kBoyleIntroResponse2Choice2Lines,
+				lineError = runtime.playDialogueEntrySequence(kBoyleIntroResponse2Choice2Lines,
 					ARRAYSIZE(kBoyleIntroResponse2Choice2Lines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 
-			lineError = playBoyleLine(0x80, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x80, "BOYLE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignBoyleTopicBuffer(0x9);
+			topics.assign(0x9);
 			seedButtonTopicBuffer = true;
 		}
 
 		if (seedButtonTopicBuffer && hasBoylesButton)
-			assignBoyleTopicBuffer(0xa);
+			topics.assign(0xa);
 	} else {
-		lineError = playBoyleLine(0x23c);
+		lineError = runtime.playDialogueLineWithVariant(0x23c, "BOYLE", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (hasBoylesButton)
-			assignBoyleTopicBuffer(0xb);
+			topics.assign(0xb);
 	}
 
 	if ((sharedState.discussedNoteCheckbookEvidence != 0 ||
 				sharedState.dwayneDiscussedBoylesButton) &&
 			!state.evidenceFollowupShown) {
 		state.evidenceFollowupShown = true;
-		lineError = playBoyleLine(0x210, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x210, "BOYLE", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (sharedState.discussedNoteCheckbookEvidence != 0) {
-			lineError = playBoyleLine(0x21a, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x21a, "BOYLE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 		if (sharedState.dwayneDiscussedBoylesButton) {
-			lineError = playBoyleLine(0x215, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x215, "BOYLE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 
 		if (!runtime.startupScript().getFlagValue(kHaveLodgeAppFlag)) {
-			lineError = playBoyleLine(0x21f, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x21f, "BOYLE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -433,34 +409,34 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playBoyleLine(0x22a, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x22a, "BOYLE", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else if (responseIndex == 2) {
-				lineError = playBoyleLine(0x22e, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x22e, "BOYLE", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 		} else {
-			lineError = playBoyleLine(0x233, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x233, "BOYLE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 
-		clearBoyleTopicBuffer();
+		topics.clear();
 	}
 
 	if (runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED") &&
 			!state.stephMidgameShown) {
 		state.stephMidgameShown = true;
-		lineError = playBoyleLine(0x267);
+		lineError = runtime.playDialogueLineWithVariant(0x267, "BOYLE", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (runtime.startupScript().getFlagValue("DAY_5") && !state.dayFiveShown) {
 		state.dayFiveShown = true;
-		lineError = playBoyleLine(0x26e, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x26e, "BOYLE", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -468,8 +444,7 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("BURNED_TV_STATION") &&
 			!state.burnedTvStationShown) {
 		state.burnedTvStationShown = true;
-		lineError = playSequence(kBoyleFollowupBurnedTvLines,
-			ARRAYSIZE(kBoyleFollowupBurnedTvLines));
+		lineError = runtime.playDialogueEntrySequence(kBoyleFollowupBurnedTvLines, ARRAYSIZE(kBoyleFollowupBurnedTvLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -483,39 +458,37 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (runtime.matchesResponseLine(selectedTopic, 0xd) ||
 				runtime.matchesResponseLine(selectedTopic, 0x20)) {
-			return playBoyleLine(0x100, 1);
+			return runtime.playDialogueLineWithVariant(0x100, "BOYLE", 1);
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0xe)) {
-			lineError = playSequence(kBoyleKeywordTopic0eLines,
-				ARRAYSIZE(kBoyleKeywordTopic0eLines));
+			lineError = runtime.playDialogueEntrySequence(kBoyleKeywordTopic0eLines, ARRAYSIZE(kBoyleKeywordTopic0eLines));
 		} else if (runtime.matchesAnyResponseLine(selectedTopic, kBoyleKeywordTopic0fTo12Lines,
 				ARRAYSIZE(kBoyleKeywordTopic0fTo12Lines))) {
-			lineError = playBoyleLine(0xab);
+			lineError = runtime.playDialogueLineWithVariant(0xab, "BOYLE", 0);
 		} else if (runtime.matchesAnyResponseLine(selectedTopic, kBoyleKeywordTopic13To14Lines,
 				ARRAYSIZE(kBoyleKeywordTopic13To14Lines))) {
-			lineError = playBoyleLine(0xb6, 3);
+			lineError = runtime.playDialogueLineWithVariant(0xb6, "BOYLE", 3);
 		} else if (runtime.matchesAnyResponseLine(selectedTopic, kBoyleKeywordTopic15To16Lines,
 				ARRAYSIZE(kBoyleKeywordTopic15To16Lines))) {
 			lineError = runtime.startupScript().getCurrentStoryDayIndex() >= 3
-				? playBoyleLine(0xc6)
-				: playBoyleLine(0x243);
+				? runtime.playDialogueLineWithVariant(0xc6, "BOYLE", 0)
+				: runtime.playDialogueLineWithVariant(0x243, "BOYLE", 0);
 		} else if (runtime.matchesResponseLine(selectedTopic, 0x17)) {
-			lineError = playSequence(kBoyleKeywordTopic17Lines,
-				ARRAYSIZE(kBoyleKeywordTopic17Lines));
+			lineError = runtime.playDialogueEntrySequence(kBoyleKeywordTopic17Lines, ARRAYSIZE(kBoyleKeywordTopic17Lines));
 		} else if (runtime.matchesAnyResponseLine(selectedTopic, kBoyleKeywordTopic18To1bLines,
 				ARRAYSIZE(kBoyleKeywordTopic18To1bLines))) {
 			if (!sharedState.dialogueStateD2eb0) {
 				lineError = runtime.playDialogueFst(kDialogueC019BFstPath);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playBoyleLine(0x10e);
+				lineError = runtime.playDialogueLineWithVariant(0x10e, "BOYLE", 0);
 			} else {
-				lineError = playBoyleLine(0x115, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x115, "BOYLE", 1);
 			}
 		} else if (runtime.matchesResponseLine(selectedTopic, 0x1d) ||
 				selectedTopic.equalsIgnoreCase(kBoyleArsonistTopic)) {
-			lineError = playBoyleLine(0x11c);
+			lineError = runtime.playDialogueLineWithVariant(0x11c, "BOYLE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -525,18 +498,18 @@ Common::Error BoyleDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playSequence(kBoyleKeywordArsonResponse1Lines,
+				lineError = runtime.playDialogueEntrySequence(kBoyleKeywordArsonResponse1Lines,
 					ARRAYSIZE(kBoyleKeywordArsonResponse1Lines));
 			} else if (responseIndex == 2) {
-				lineError = playBoyleLine(0x135);
+				lineError = runtime.playDialogueLineWithVariant(0x135, "BOYLE", 0);
 			} else {
 				lineError = Common::kNoError;
 			}
 		} else if (runtime.matchesResponseLine(selectedTopic, 0x1f)) {
-			lineError = playSequence(kBoyleHaveLodgeAppEvidenceLines,
+			lineError = runtime.playDialogueEntrySequence(kBoyleHaveLodgeAppEvidenceLines,
 				ARRAYSIZE(kBoyleHaveLodgeAppEvidenceLines));
 		} else {
-			lineError = playBoyleLine(0x243);
+			lineError = runtime.playDialogueLineWithVariant(0x243, "BOYLE", 0);
 		}
 
 		if (lineError.getCode() != Common::kNoError)
diff --git a/engines/harvester/npc/boyle_dialogue.h b/engines/harvester/npc/boyle_dialogue.h
index e9453e8217e..b205ab65c90 100644
--- a/engines/harvester/npc/boyle_dialogue.h
+++ b/engines/harvester/npc/boyle_dialogue.h
@@ -37,7 +37,6 @@ public:
 		bool burnedTvStationShown = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = BoyleRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.introPending);
diff --git a/engines/harvester/npc/buster_dialogue.cpp b/engines/harvester/npc/buster_dialogue.cpp
index d3f2e85fe71..598d4f425f1 100644
--- a/engines/harvester/npc/buster_dialogue.cpp
+++ b/engines/harvester/npc/buster_dialogue.cpp
@@ -32,8 +32,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kBusterNpc = "BUSTER";
-static const char *const kPcSpeaker = "PC";
 static const char *const kDialogueC058FstPath = "GRAPHIC/FST/C058.FST";
 static const char *const kDialogueC085FstPath = "GRAPHIC/FST/C085.FST";
 static const char *const kDialogueC086FstPath = "GRAPHIC/FST/C086.FST";
@@ -71,130 +69,117 @@ static const int kBusterTopic0x48Line = 0x48;
 static const int kBusterTopic0x49Line = 0x49;
 
 static const DialogueLineEntry kBusterCorpsePhotoLines[] = {
-	{ 0x1884, kBusterNpc, 0 },
-	{ 0x1889, kPcSpeaker, 4 },
-	{ 0x188e, kBusterNpc, 2 },
-	{ 0x1895, kBusterNpc, 2 }
+	{ 0x1884, "BUSTER", 0 },
+	{ 0x1889, "PC", 4 },
+	{ 0x188e, "BUSTER", 2 },
+	{ 0x1895, "BUSTER", 2 }
 };
 
 static const DialogueLineEntry kBusterIntroLines[] = {
-	{ 0x163c, kBusterNpc, 2 },
-	{ 0x1640, kPcSpeaker, 0 },
-	{ 0x1644, kBusterNpc, 0 },
-	{ 0x1649, kPcSpeaker, 0 },
-	{ 0x164d, kBusterNpc, 0 },
-	{ 0x1654, kPcSpeaker, 0 },
-	{ 0x1658, kBusterNpc, 0 },
-	{ 0x165e, kPcSpeaker, 4 },
-	{ 0x1662, kBusterNpc, 0 },
-	{ 0x1669, kPcSpeaker, 0 },
-	{ 0x166e, kBusterNpc, 0 },
-	{ 0x1673, kPcSpeaker, 0 },
-	{ 0x1677, kBusterNpc, 0 },
-	{ 0x167e, kBusterNpc, 0 },
-	{ 0x1682, kBusterNpc, 2 },
-	{ 0x168e, kBusterNpc, 0 }
+	{ 0x163c, "BUSTER", 2 },
+	{ 0x1640, "PC", 0 },
+	{ 0x1644, "BUSTER", 0 },
+	{ 0x1649, "PC", 0 },
+	{ 0x164d, "BUSTER", 0 },
+	{ 0x1654, "PC", 0 },
+	{ 0x1658, "BUSTER", 0 },
+	{ 0x165e, "PC", 4 },
+	{ 0x1662, "BUSTER", 0 },
+	{ 0x1669, "PC", 0 },
+	{ 0x166e, "BUSTER", 0 },
+	{ 0x1673, "PC", 0 },
+	{ 0x1677, "BUSTER", 0 },
+	{ 0x167e, "BUSTER", 0 },
+	{ 0x1682, "BUSTER", 2 },
+	{ 0x168e, "BUSTER", 0 }
 };
 
 static const DialogueLineEntry kBusterSecondIntroPreludeLines[] = {
-	{ 0x1828, kBusterNpc, 2 },
-	{ 0x182c, kPcSpeaker, 2 },
-	{ 0x1830, kBusterNpc, 2 }
+	{ 0x1828, "BUSTER", 2 },
+	{ 0x182c, "PC", 2 },
+	{ 0x1830, "BUSTER", 2 }
 };
 
 static const DialogueLineEntry kBusterThirdIntroLines[] = {
-	{ 0x18c5, kBusterNpc, 2 },
-	{ 0x18c9, kPcSpeaker, 4 },
-	{ 0x18cd, kBusterNpc, 2 }
+	{ 0x18c5, "BUSTER", 2 },
+	{ 0x18c9, "PC", 4 },
+	{ 0x18cd, "BUSTER", 2 }
 };
 
 static const DialogueLineEntry kBusterTopic0x28Or0x29LinesSequence[] = {
-	{ 0x1698, kBusterNpc, 0 },
-	{ 0x169c, kPcSpeaker, 4 },
-	{ 0x16a1, kBusterNpc, 0 }
+	{ 0x1698, "BUSTER", 0 },
+	{ 0x169c, "PC", 4 },
+	{ 0x16a1, "BUSTER", 0 }
 };
 
 static const DialogueLineEntry kBusterTopic0x28Or0x29TailLines[] = {
-	{ 0x16ad, kBusterNpc, 2 },
-	{ 0x16b6, kBusterNpc, 0 }
+	{ 0x16ad, "BUSTER", 2 },
+	{ 0x16b6, "BUSTER", 0 }
 };
 
 static const DialogueLineEntry kBusterTopic0x32Lines[] = {
-	{ 0x170a, kPcSpeaker, 0 },
-	{ 0x170f, kBusterNpc, 0 },
-	{ 0x1710, kBusterNpc, 0 },
-	{ 0x1711, kBusterNpc, 0 },
-	{ 0x1712, kBusterNpc, 0 }
+	{ 0x170a, "PC", 0 },
+	{ 0x170f, "BUSTER", 0 },
+	{ 0x1710, "BUSTER", 0 },
+	{ 0x1711, "BUSTER", 0 },
+	{ 0x1712, "BUSTER", 0 }
 };
 
 static const DialogueLineEntry kBusterTopic0x34Lines[] = {
-	{ 0x1727, kBusterNpc, 0 },
-	{ 0x1728, kBusterNpc, 0 },
-	{ 0x1729, kBusterNpc, 0 },
-	{ 0x172a, kBusterNpc, 0 },
-	{ 0x172b, kBusterNpc, 0 }
+	{ 0x1727, "BUSTER", 0 },
+	{ 0x1728, "BUSTER", 0 },
+	{ 0x1729, "BUSTER", 0 },
+	{ 0x172a, "BUSTER", 0 },
+	{ 0x172b, "BUSTER", 0 }
 };
 
 static const DialogueLineEntry kBusterTopic0x36Lines[] = {
-	{ 0x173d, kPcSpeaker, 0 },
-	{ 0x1742, kBusterNpc, 0 },
-	{ 0x1749, kPcSpeaker, 0 },
-	{ 0x174d, kBusterNpc, 0 },
-	{ 0x174e, kBusterNpc, 0 },
-	{ 0x174f, kBusterNpc, 0 },
-	{ 0x1750, kBusterNpc, 2 },
-	{ 0x1762, kBusterNpc, 2 }
+	{ 0x173d, "PC", 0 },
+	{ 0x1742, "BUSTER", 0 },
+	{ 0x1749, "PC", 0 },
+	{ 0x174d, "BUSTER", 0 },
+	{ 0x174e, "BUSTER", 0 },
+	{ 0x174f, "BUSTER", 0 },
+	{ 0x1750, "BUSTER", 2 },
+	{ 0x1762, "BUSTER", 2 }
 };
 
 static const DialogueLineEntry kBusterTopic0x38ResponseTwoLines[] = {
-	{ 0x1787, kBusterNpc, 0 },
-	{ 0x1791, kBusterNpc, 0 },
-	{ 0x1796, kPcSpeaker, 0 },
-	{ 0x179d, kBusterNpc, 0 }
+	{ 0x1787, "BUSTER", 0 },
+	{ 0x1791, "BUSTER", 0 },
+	{ 0x1796, "PC", 0 },
+	{ 0x179d, "BUSTER", 0 }
 };
 
 static const DialogueLineEntry kBusterTopic0x3bTailLines[] = {
-	{ 0x17bb, kBusterNpc, 0 },
-	{ 0x17c3, kBusterNpc, 0 }
+	{ 0x17bb, "BUSTER", 0 },
+	{ 0x17c3, "BUSTER", 0 }
 };
 
 static const DialogueLineEntry kBusterTopic0x3eLines[] = {
-	{ 0x17cc, kBusterNpc, 0 },
-	{ 0x17cd, kBusterNpc, 0 },
-	{ 0x17ce, kBusterNpc, 0 },
-	{ 0x17db, kBusterNpc, 0 }
+	{ 0x17cc, "BUSTER", 0 },
+	{ 0x17cd, "BUSTER", 0 },
+	{ 0x17ce, "BUSTER", 0 },
+	{ 0x17db, "BUSTER", 0 }
 };
 
 static const DialogueLineEntry kBusterTopic0x40Or0x41PreludeLines[] = {
-	{ 0x17e4, kBusterNpc, 0 },
-	{ 0x17e5, kBusterNpc, 0 },
-	{ 0x17e6, kBusterNpc, 0 },
-	{ 0x17e7, kBusterNpc, 0 },
-	{ 0x17f7, kBusterNpc, 0 }
+	{ 0x17e4, "BUSTER", 0 },
+	{ 0x17e5, "BUSTER", 0 },
+	{ 0x17e6, "BUSTER", 0 },
+	{ 0x17e7, "BUSTER", 0 },
+	{ 0x17f7, "BUSTER", 0 }
 };
 
 } // End of namespace
 
-bool BusterDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kBusterNpc);
-}
-
 Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	Common::String topicBuffer;
 	int topicBufferLineIndex = -1;
 
-	auto assignBusterTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(topicBuffer, topicBufferLineIndex,
-			responseLineIndex, "Buster topic buffer");
-	};
-	auto playBusterLine = [&](int wavId, int headVariant = 0,
-			const char *speakerId = kBusterNpc) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, speakerId, headVariant);
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
+	DialogueTopicBuffer topics(runtime, topicBuffer, topicBufferLineIndex, "Buster topic buffer");
+
 	auto clearScreenToBlack = [&]() {
 		Graphics::Screen *screen = runtime.engine().getScreen();
 		if (!screen)
@@ -204,7 +189,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		screen->update();
 	};
 	auto playFatalBusterBranch = [&](int wavId, int headVariant = 0) -> Common::Error {
-		Common::Error lineError = playBusterLine(wavId, headVariant);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(wavId, "BUSTER", headVariant);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -230,17 +215,17 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
 			sharedState.discussedWhaleyHerrillPhoto = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownPhotoOfWhaleyHerrill, true);
-			return playBusterLine(0x187b);
+			return runtime.playDialogueLineWithVariant(0x187b, "BUSTER", 0);
 		}
 		if (isCorpsePhoto(usedItemName)) {
 			sharedState.discussedCasketPhotoEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownPhotoOfCorpse, true);
-			return playSequence(kBusterCorpsePhotoLines, ARRAYSIZE(kBusterCorpsePhotoLines));
+			return runtime.playDialogueEntrySequence(kBusterCorpsePhotoLines, ARRAYSIZE(kBusterCorpsePhotoLines));
 		}
 		if (isBlackmailEvidence(usedItemName)) {
 			sharedState.discussedNoteCheckbookEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownEvidenceOfBlackmail, true);
-			Common::Error lineError = playBusterLine(0x189b);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x189b, "BUSTER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -251,38 +236,38 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playBusterLine(0x18a6, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x18a6, "BUSTER", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playBusterLine(0x18aa, 0, kPcSpeaker);
+				lineError = runtime.playDialogueLineWithVariant(0x18aa, "PC", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				return playBusterLine(0x18ae);
+				return runtime.playDialogueLineWithVariant(0x18ae, "BUSTER", 0);
 			}
 			if (responseIndex == 2) {
-				lineError = playBusterLine(0x18b5, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x18b5, "BUSTER", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				return playBusterLine(0x18be, 2);
+				return runtime.playDialogueLineWithVariant(0x18be, "BUSTER", 2);
 			}
 			return Common::kNoError;
 		}
-		return playBusterLine(0x1875);
+		return runtime.playDialogueLineWithVariant(0x1875, "BUSTER", 0);
 	}
 
 	if (_state.introPending) {
 		_state.introPending = false;
 		_state.secondIntroPending = true;
-		Common::Error lineError = playSequence(kBusterIntroLines, ARRAYSIZE(kBusterIntroLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kBusterIntroLines, ARRAYSIZE(kBusterIntroLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		assignBusterTopicBuffer(kBusterIntroTopicBufferLine);
+		topics.assign(kBusterIntroTopicBufferLine);
 	} else if (_state.secondIntroPending) {
 		_state.secondIntroPending = false;
 		_state.thirdIntroPending = true;
-		Common::Error lineError = playSequence(
-			kBusterSecondIntroPreludeLines, ARRAYSIZE(kBusterSecondIntroPreludeLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kBusterSecondIntroPreludeLines,
+			ARRAYSIZE(kBusterSecondIntroPreludeLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -295,12 +280,12 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (responseIndex == 1)
 			return playFatalBusterBranch(0x183a, 2);
 		if (responseIndex == 2) {
-			lineError = playBusterLine(0x1841);
+			lineError = runtime.playDialogueLineWithVariant(0x1841, "BUSTER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			if (!_state.secondIntroResponseTwoSeen) {
-				lineError = playBusterLine(0x185e);
+				lineError = runtime.playDialogueLineWithVariant(0x185e, "BUSTER", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -308,7 +293,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				_state.secondIntroPending = true;
 				_state.thirdIntroPending = false;
 			} else {
-				lineError = playBusterLine(0x1845);
+				lineError = runtime.playDialogueLineWithVariant(0x1845, "BUSTER", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -321,7 +306,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				if (followupResponseIndex == 1)
 					return playFatalBusterBranch(0x1850);
 				if (followupResponseIndex == 2) {
-					lineError = playBusterLine(0x1858);
+					lineError = runtime.playDialogueLineWithVariant(0x1858, "BUSTER", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				}
@@ -329,9 +314,9 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 	} else if (_state.thirdIntroPending) {
 		_state.thirdIntroPending = false;
-		return playSequence(kBusterThirdIntroLines, ARRAYSIZE(kBusterThirdIntroLines));
+		return runtime.playDialogueEntrySequence(kBusterThirdIntroLines, ARRAYSIZE(kBusterThirdIntroLines));
 	} else {
-		return playBusterLine(0x1868);
+		return runtime.playDialogueLineWithVariant(0x1868, "BUSTER", 0);
 	}
 
 	for (;;) {
@@ -343,32 +328,32 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return Common::kNoError;
 
 		if (runtime.matchesResponseLine(selectedTopic, kBusterExitTopicLine))
-			return playBusterLine(0x1818);
+			return runtime.playDialogueLineWithVariant(0x1818, "BUSTER", 0);
 
 		if (runtime.matchesAnyResponseLine(selectedTopic, kBusterTopic0x28Or0x29Lines,
 				ARRAYSIZE(kBusterTopic0x28Or0x29Lines))) {
-			Common::Error lineError = playSequence(
-				kBusterTopic0x28Or0x29LinesSequence, ARRAYSIZE(kBusterTopic0x28Or0x29LinesSequence));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kBusterTopic0x28Or0x29LinesSequence,
+				ARRAYSIZE(kBusterTopic0x28Or0x29LinesSequence));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC085FstPath);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playSequence(
-				kBusterTopic0x28Or0x29TailLines, ARRAYSIZE(kBusterTopic0x28Or0x29TailLines));
+			lineError = runtime.playDialogueEntrySequence(kBusterTopic0x28Or0x29TailLines,
+				ARRAYSIZE(kBusterTopic0x28Or0x29TailLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignBusterTopicBuffer(0x2b);
+			topics.assign(0x2b);
 			continue;
 		}
 
 		if (runtime.matchesAnyResponseLine(selectedTopic, kBusterTopic0x2cOr0x2dLines,
 				ARRAYSIZE(kBusterTopic0x2cOr0x2dLines))) {
-			Common::Error lineError = playBusterLine(0x16be);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x16be, "BUSTER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playBusterLine(0x16c5);
+			lineError = runtime.playDialogueLineWithVariant(0x16c5, "BUSTER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -381,7 +366,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseIndex == 1)
 				return playFatalBusterBranch(0x16d0);
 			if (responseIndex == 2) {
-				lineError = playBusterLine(0x16d6);
+				lineError = runtime.playDialogueLineWithVariant(0x16d6, "BUSTER", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -392,7 +377,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 					return responseError;
 
 				if (followupResponseIndex == 1) {
-					lineError = playBusterLine(0x16e2);
+					lineError = runtime.playDialogueLineWithVariant(0x16e2, "BUSTER", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 
@@ -405,11 +390,11 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 					if (nestedResponseIndex == 1)
 						return playFatalBusterBranch(0x16ef);
 					if (nestedResponseIndex == 2) {
-						lineError = playBusterLine(0x16f6);
+						lineError = runtime.playDialogueLineWithVariant(0x16f6, "BUSTER", 0);
 						if (lineError.getCode() != Common::kNoError)
 							return lineError;
 
-						assignBusterTopicBuffer(0x31);
+						topics.assign(0x31);
 					}
 				} else if (followupResponseIndex == 2) {
 					return playFatalBusterBranch(0x1702);
@@ -419,37 +404,34 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, kBusterTopic0x32Line)) {
-			Common::Error lineError = playSequence(
-				kBusterTopic0x32Lines, ARRAYSIZE(kBusterTopic0x32Lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kBusterTopic0x32Lines, ARRAYSIZE(kBusterTopic0x32Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignBusterTopicBuffer(kBusterTopic0x32TopicBufferLine);
+			topics.assign(kBusterTopic0x32TopicBufferLine);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, kBusterTopic0x34Line)) {
-			Common::Error lineError = playSequence(
-				kBusterTopic0x34Lines, ARRAYSIZE(kBusterTopic0x34Lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kBusterTopic0x34Lines, ARRAYSIZE(kBusterTopic0x34Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignBusterTopicBuffer(kBusterTopic0x34TopicBufferLine);
+			topics.assign(kBusterTopic0x34TopicBufferLine);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, kBusterTopic0x36Line)) {
-			Common::Error lineError = playSequence(
-				kBusterTopic0x36Lines, ARRAYSIZE(kBusterTopic0x36Lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kBusterTopic0x36Lines, ARRAYSIZE(kBusterTopic0x36Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignBusterTopicBuffer(kBusterTopic0x36TopicBufferLine);
+			topics.assign(kBusterTopic0x36TopicBufferLine);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, kBusterTopic0x38Line)) {
-			Common::Error lineError = playBusterLine(0x176b);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x176b, "BUSTER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC058FstPath);
@@ -463,20 +445,20 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1)
-				return playBusterLine(0x1781);
+				return runtime.playDialogueLineWithVariant(0x1781, "BUSTER", 0);
 			if (responseIndex == 2) {
-				lineError = playSequence(
-					kBusterTopic0x38ResponseTwoLines, ARRAYSIZE(kBusterTopic0x38ResponseTwoLines));
+				lineError = runtime.playDialogueEntrySequence(kBusterTopic0x38ResponseTwoLines,
+					ARRAYSIZE(kBusterTopic0x38ResponseTwoLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 
-			assignBusterTopicBuffer(kBusterTopic0x38TopicBufferLine);
+			topics.assign(kBusterTopic0x38TopicBufferLine);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, kBusterTopic0x3bLine)) {
-			Common::Error lineError = playBusterLine(0x17a5);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x17a5, "BUSTER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -487,36 +469,35 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playBusterLine(0x17b3);
+				lineError = runtime.playDialogueLineWithVariant(0x17b3, "BUSTER", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else if (responseIndex == 2) {
-				lineError = playBusterLine(0x17b7);
+				lineError = runtime.playDialogueLineWithVariant(0x17b7, "BUSTER", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 
-			lineError = playSequence(kBusterTopic0x3bTailLines, ARRAYSIZE(kBusterTopic0x3bTailLines));
+			lineError = runtime.playDialogueEntrySequence(kBusterTopic0x3bTailLines, ARRAYSIZE(kBusterTopic0x3bTailLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignBusterTopicBuffer(kBusterTopic0x3bTopicBufferLine);
+			topics.assign(kBusterTopic0x3bTopicBufferLine);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, kBusterTopic0x3eLine)) {
-			Common::Error lineError = playSequence(
-				kBusterTopic0x3eLines, ARRAYSIZE(kBusterTopic0x3eLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kBusterTopic0x3eLines, ARRAYSIZE(kBusterTopic0x3eLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignBusterTopicBuffer(kBusterTopic0x3eTopicBufferLine);
+			topics.assign(kBusterTopic0x3eTopicBufferLine);
 			continue;
 		}
 
 		if (runtime.matchesAnyResponseLine(selectedTopic, kBusterTopic0x40Or0x41Lines,
 				ARRAYSIZE(kBusterTopic0x40Or0x41Lines))) {
-			Common::Error lineError = playSequence(kBusterTopic0x40Or0x41PreludeLines,
+			Common::Error lineError = runtime.playDialogueEntrySequence(kBusterTopic0x40Or0x41PreludeLines,
 				ARRAYSIZE(kBusterTopic0x40Or0x41PreludeLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -530,22 +511,22 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseIndex == 1)
 				return playFatalBusterBranch(0x1804);
 			if (responseIndex == 2) {
-				lineError = playBusterLine(0x180b);
+				lineError = runtime.playDialogueLineWithVariant(0x180b, "BUSTER", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
-				assignBusterTopicBuffer(kBusterTopic0x40TopicBufferLine);
+				topics.assign(kBusterTopic0x40TopicBufferLine);
 			}
 			continue;
 		}
 
 		if (runtime.matchesAnyResponseLine(selectedTopic, kBusterTopic0x44Or0x45Or0x46Lines,
 				ARRAYSIZE(kBusterTopic0x44Or0x45Or0x46Lines))) {
-			Common::Error lineError = playBusterLine(0x181e, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x181e, "BUSTER", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignBusterTopicBuffer(kBusterTopic0x44TopicBufferLine);
+			topics.assign(kBusterTopic0x44TopicBufferLine);
 			continue;
 		}
 
@@ -553,7 +534,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return playFatalBusterBranch(0x18df, 2);
 
 		if (runtime.matchesResponseLine(selectedTopic, kBusterTopic0x49Line)) {
-			Common::Error lineError = playBusterLine(0x186f);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x186f, "BUSTER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
diff --git a/engines/harvester/npc/buster_dialogue.h b/engines/harvester/npc/buster_dialogue.h
index dde1dd192ba..6604bd2b578 100644
--- a/engines/harvester/npc/buster_dialogue.h
+++ b/engines/harvester/npc/buster_dialogue.h
@@ -35,7 +35,6 @@ public:
 		bool secondIntroResponseTwoSeen = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = BusterRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.introPending);
diff --git a/engines/harvester/npc/butcher_dialogue.cpp b/engines/harvester/npc/butcher_dialogue.cpp
index 39521964311..9cfd4f3bada 100644
--- a/engines/harvester/npc/butcher_dialogue.cpp
+++ b/engines/harvester/npc/butcher_dialogue.cpp
@@ -29,8 +29,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kButcherNpc = "BUTCHER";
-static const char *const kPcSpeaker = "PC";
 static const char *const kDialogueC060FstPath = "GRAPHIC/FST/C060.FST";
 static const char *const kDialogueC060AFstPath = "GRAPHIC/FST/C060A.FST";
 static const char *const kDialogueC119FstPath = "GRAPHIC/FST/C119.FST";
@@ -42,48 +40,44 @@ static const char *const kDialogueC123AFstPath = "GRAPHIC/FST/C123A.FST";
 static const char *const kDialogueC123BFstPath = "GRAPHIC/FST/C123B.FST";
 
 static const DialogueLineEntry kButcherWhaleyPhotoLines[] = {
-	{ 0x1a5a, kButcherNpc, 0 },
-	{ 0x1a5e, kPcSpeaker, 0 },
-	{ 0x1a62, kButcherNpc, 1 }
+	{ 0x1a5a, "BUTCHER", 0 },
+	{ 0x1a5e, "PC", 0 },
+	{ 0x1a62, "BUTCHER", 1 }
 };
 
 static const DialogueLineEntry kButcherCasketPhotoLines[] = {
-	{ 0x1a69, kButcherNpc, 0 },
-	{ 0x1a6d, kPcSpeaker, 4 },
-	{ 0x1a71, kButcherNpc, 1 },
-	{ 0x1a76, kPcSpeaker, 0 },
-	{ 0x1a7a, kButcherNpc, 0 }
+	{ 0x1a69, "BUTCHER", 0 },
+	{ 0x1a6d, "PC", 4 },
+	{ 0x1a71, "BUTCHER", 1 },
+	{ 0x1a76, "PC", 0 },
+	{ 0x1a7a, "BUTCHER", 0 }
 };
 
 static const DialogueLineEntry kButcherLedgerLines[] = {
-	{ 0x1a80, kButcherNpc, 0 },
-	{ 0x1a85, kPcSpeaker, 0 },
-	{ 0x1a8a, kButcherNpc, 0 },
-	{ 0x1a92, kButcherNpc, 0 },
-	{ 0x1a96, kButcherNpc, 2 }
+	{ 0x1a80, "BUTCHER", 0 },
+	{ 0x1a85, "PC", 0 },
+	{ 0x1a8a, "BUTCHER", 0 },
+	{ 0x1a92, "BUTCHER", 0 },
+	{ 0x1a96, "BUTCHER", 2 }
 };
 
 static const DialogueLineEntry kButcherOpeningQuestionLines[] = {
-	{ 0x192a, kPcSpeaker, 0 },
-	{ 0x192f, kButcherNpc, 0 },
-	{ 0x1933, kPcSpeaker, 0 },
-	{ 0x1938, kButcherNpc, 2 }
+	{ 0x192a, "PC", 0 },
+	{ 0x192f, "BUTCHER", 0 },
+	{ 0x1933, "PC", 0 },
+	{ 0x1938, "BUTCHER", 2 }
 };
 
 static const DialogueLineEntry kButcherLodgeTopicLines[] = {
-	{ 0x1a07, kButcherNpc, 0 },
-	{ 0x1a0d, kPcSpeaker, 0 },
-	{ 0x1a11, kButcherNpc, 1 },
-	{ 0x1a18, kPcSpeaker, 0 },
-	{ 0x1a1c, kButcherNpc, 1 }
+	{ 0x1a07, "BUTCHER", 0 },
+	{ 0x1a0d, "PC", 0 },
+	{ 0x1a11, "BUTCHER", 1 },
+	{ 0x1a18, "PC", 0 },
+	{ 0x1a1c, "BUTCHER", 1 }
 };
 
 } // End of namespace
 
-bool ButcherDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kButcherNpc);
-}
-
 Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	ButcherRoomDialogueState &state = _state;
@@ -92,23 +86,8 @@ Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	state.currentTopicBuffer.clear();
 	state.currentTopicBufferLineIndex = -1;
 
-	auto assignButcherTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(butcherTopicBuffer, butcherTopicBufferLineIndex,
-			responseLineIndex, "Butcher topic buffer");
-	};
-	auto clearButcherTopicBuffer = [&]() {
-		butcherTopicBuffer.clear();
-		butcherTopicBufferLineIndex = -1;
-	};
-	auto playButcherLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kButcherNpc, headVariant);
-	};
-	auto playPcLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kPcSpeaker, headVariant);
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
+	DialogueTopicBuffer topics(runtime, butcherTopicBuffer, butcherTopicBufferLineIndex, "Butcher topic buffer");
+
 	auto hasMeatPermission = [&]() {
 		return runtime.startupScript().isObjectInInventory("MEAT_PERMISSION0") ||
 			runtime.startupScript().isObjectInInventory("MEAT_PERMISSION");
@@ -119,14 +98,14 @@ Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			sharedState.discussedWhaleyHerrillPhoto = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(
 				DialogueFlags::kShownPhotoOfWhaleyHerrill, true);
-			return playSequence(kButcherWhaleyPhotoLines, ARRAYSIZE(kButcherWhaleyPhotoLines));
+			return runtime.playDialogueEntrySequence(kButcherWhaleyPhotoLines, ARRAYSIZE(kButcherWhaleyPhotoLines));
 		}
 		if (usedItemName.equalsIgnoreCase("CASKET_PHOTO") ||
 				usedItemName.equalsIgnoreCase("CASKET_PHOTOCOPY")) {
 			sharedState.discussedCasketPhotoEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(
 				DialogueFlags::kShownPhotoOfCorpse, true);
-			return playSequence(kButcherCasketPhotoLines, ARRAYSIZE(kButcherCasketPhotoLines));
+			return runtime.playDialogueEntrySequence(kButcherCasketPhotoLines, ARRAYSIZE(kButcherCasketPhotoLines));
 		}
 		if ((usedItemName.equalsIgnoreCase("LEDGER") ||
 					usedItemName.equalsIgnoreCase("LEDGER2")) &&
@@ -134,7 +113,7 @@ Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			sharedState.discussedLedgerEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(
 				DialogueFlags::kShownLedgersToAnyone, true);
-			return playSequence(kButcherLedgerLines, ARRAYSIZE(kButcherLedgerLines));
+			return runtime.playDialogueEntrySequence(kButcherLedgerLines, ARRAYSIZE(kButcherLedgerLines));
 		}
 		if (usedItemName.equalsIgnoreCase("NOTE") ||
 				usedItemName.equalsIgnoreCase("NOTE_PHOTOCOPY") ||
@@ -143,10 +122,10 @@ Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			sharedState.discussedNoteCheckbookEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(
 				DialogueFlags::kShownEvidenceOfBlackmail, true);
-			return playButcherLine(0x1a9e);
+			return runtime.playDialogueLineWithVariant(0x1a9e, "BUTCHER", 0);
 		}
 		if (usedItemName.equalsIgnoreCase("MEAT_PERMISSION")) {
-			Common::Error lineError = playButcherLine(0x1ac4, 1);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x1ac4, "BUTCHER", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -155,7 +134,7 @@ Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 
-			lineError = playButcherLine(0x1ace, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x1ace, "BUTCHER", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC060AFstPath);
@@ -168,50 +147,50 @@ Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return Common::kNoError;
 		}
 
-		return playButcherLine(0x1a54);
+		return runtime.playDialogueLineWithVariant(0x1a54, "BUTCHER", 0);
 	}
 
 	if (state.introPending) {
 		state.introPending = false;
 
-		Common::Error lineError = playButcherLine(0x1902);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x1902, "BUTCHER", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playPcLine(0x1906);
+		lineError = runtime.playDialogueLineWithVariant(0x1906, "PC", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playButcherLine(0x190a);
+		lineError = runtime.playDialogueLineWithVariant(0x190a, "BUTCHER", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playPcLine(sharedState.dialogueStateD2ed0 ? 0x1913 : 0x190f);
+		lineError = runtime.playDialogueLineWithVariant(sharedState.dialogueStateD2ed0 ? 0x1913 : 0x190f, "PC", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playButcherLine(0x1917, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x1917, "BUTCHER", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playPcLine(0x191d);
+		lineError = runtime.playDialogueLineWithVariant(0x191d, "PC", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playButcherLine(0x1921, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x1921, "BUTCHER", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		assignButcherTopicBuffer(0x260);
+		topics.assign(0x260);
 	} else {
-		Common::Error lineError = playButcherLine(0x1a47);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x1a47, "BUTCHER", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
 		if (!hasMeatPermission() && runtime.startupScript().getCurrentStoryDayIndex() <= 3)
-			assignButcherTopicBuffer(0x261);
+			topics.assign(0x261);
 		else
-			clearButcherTopicBuffer();
+			topics.clear();
 	}
 
 	if (runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED") &&
 			!state.stephMidgameShown) {
 		state.stephMidgameShown = true;
-		Common::Error lineError = playButcherLine(0x1aa7, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x1aa7, "BUTCHER", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		lineError = runtime.playDialogueFst(kDialogueC123BFstPath);
@@ -220,7 +199,7 @@ Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	}
 	if (runtime.startupScript().getFlagValue("DAY_5") && !state.dayFiveShown) {
 		state.dayFiveShown = true;
-		Common::Error lineError = playButcherLine(0x1ab6, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x1ab6, "BUTCHER", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		lineError = runtime.playDialogueFst(kDialogueC119CFstPath);
@@ -239,12 +218,12 @@ Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (selectedTopic.equalsIgnoreCase(runtime.genericByeTopic()) ||
 				runtime.matchesResponseLine(selectedTopic, 0x262)) {
-			return playButcherLine(0x1a41);
+			return runtime.playDialogueLineWithVariant(0x1a41, "BUTCHER", 0);
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x263)) {
-			Common::Error lineError = playSequence(
-				kButcherOpeningQuestionLines, ARRAYSIZE(kButcherOpeningQuestionLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kButcherOpeningQuestionLines,
+				ARRAYSIZE(kButcherOpeningQuestionLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -254,45 +233,45 @@ Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playButcherLine(0x1943, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x1943, "BUTCHER", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				lineError = runtime.playDialogueFst(kDialogueC119FstPath);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else if (responseIndex == 2) {
-				lineError = playButcherLine(0x1950, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x1950, "BUTCHER", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playPcLine(0x1955);
+				lineError = runtime.playDialogueLineWithVariant(0x1955, "PC", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playButcherLine(0x195a);
+				lineError = runtime.playDialogueLineWithVariant(0x195a, "BUTCHER", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
-				assignButcherTopicBuffer(hasMeatPermission() ? 0x266 : 0x265);
+				topics.assign(hasMeatPermission() ? 0x266 : 0x265);
 			}
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x267) && !hasMeatPermission()) {
-			Common::Error lineError = playPcLine(0x1964);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x1964, "PC", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playButcherLine(0x1969);
+			lineError = runtime.playDialogueLineWithVariant(0x1969, "BUTCHER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC060FstPath);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playButcherLine(0x1975);
+			lineError = runtime.playDialogueLineWithVariant(0x1975, "BUTCHER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC119AFstPath);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playButcherLine(0x1980);
+			lineError = runtime.playDialogueLineWithVariant(0x1980, "BUTCHER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -302,134 +281,134 @@ Common::Error ButcherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playButcherLine(0x198b);
+				lineError = runtime.playDialogueLineWithVariant(0x198b, "BUTCHER", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				sharedState.dadMeatPermissionState = 1;
 				(void)runtime.startupScript().addRuntimeObjectToInventory("MEAT_PERMISSION0");
-				assignButcherTopicBuffer(0x269);
+				topics.assign(0x269);
 			} else if (responseIndex == 2) {
-				lineError = playButcherLine(0x1992, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x1992, "BUTCHER", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignButcherTopicBuffer(0x26a);
+				topics.assign(0x26a);
 			}
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x26b)) {
-			Common::Error lineError = playButcherLine(0x199c);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x199c, "BUTCHER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC119BFstPath);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playButcherLine(0x19a7);
+			lineError = runtime.playDialogueLineWithVariant(0x19a7, "BUTCHER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playButcherLine(0x19ae);
+			lineError = runtime.playDialogueLineWithVariant(0x19ae, "BUTCHER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playButcherLine(0x19b3, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x19b3, "BUTCHER", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC123FstPath);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playButcherLine(0x19bf, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x19bf, "BUTCHER", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignButcherTopicBuffer(hasMeatPermission() ? 0x26d : 0x26c);
+			topics.assign(hasMeatPermission() ? 0x26d : 0x26c);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x26e)) {
-			Common::Error lineError = playButcherLine(0x19ca);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x19ca, "BUTCHER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			if (sharedState.momFatherTopicState == 0) {
-				lineError = playPcLine(0x19ce);
+				lineError = runtime.playDialogueLineWithVariant(0x19ce, "PC", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else {
-				lineError = playPcLine(0x19d2);
+				lineError = runtime.playDialogueLineWithVariant(0x19d2, "PC", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playButcherLine(0x19d6);
+				lineError = runtime.playDialogueLineWithVariant(0x19d6, "BUTCHER", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 
 			if (sharedState.dialogueStateD2ed0) {
-				lineError = playPcLine(0x19dc);
+				lineError = runtime.playDialogueLineWithVariant(0x19dc, "PC", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playButcherLine(0x19e1);
+				lineError = runtime.playDialogueLineWithVariant(0x19e1, "BUTCHER", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playPcLine(0x19e5, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x19e5, "PC", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playButcherLine(0x19e9, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x19e9, "BUTCHER", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
-				assignButcherTopicBuffer(0x26f);
+				topics.assign(0x26f);
 			}
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x270)) {
-			Common::Error lineError = playButcherLine(0x19f6);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x19f6, "BUTCHER", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC123AFstPath);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignButcherTopicBuffer(hasMeatPermission() ? 0x272 : 0x271);
+			topics.assign(hasMeatPermission() ? 0x272 : 0x271);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x273) ||
 				runtime.matchesResponseLine(selectedTopic, 0x274)) {
 			sharedState.discussedLodgeTopic = true;
-			Common::Error lineError = playSequence(
-				kButcherLodgeTopicLines, ARRAYSIZE(kButcherLodgeTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kButcherLodgeTopicLines,
+				ARRAYSIZE(kButcherLodgeTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignButcherTopicBuffer(hasMeatPermission() ? 0x276 : 0x275);
+			topics.assign(hasMeatPermission() ? 0x276 : 0x275);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x277)) {
-			Common::Error lineError = playPcLine(0x1a24, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x1a24, "PC", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playButcherLine(0x1a29, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x1a29, "BUTCHER", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			if (!sharedState.dialogueStateD2f04) {
 				sharedState.dialogueStateD2f04 = true;
-				lineError = playPcLine(0x1a2d);
+				lineError = runtime.playDialogueLineWithVariant(0x1a2d, "PC", 0);
 			} else {
-				lineError = playPcLine(0x1a31);
+				lineError = runtime.playDialogueLineWithVariant(0x1a31, "PC", 0);
 				if (lineError.getCode() == Common::kNoError)
-					lineError = playButcherLine(0x1a36, 2);
+					lineError = runtime.playDialogueLineWithVariant(0x1a36, "BUTCHER", 2);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignButcherTopicBuffer(0x278);
+			topics.assign(0x278);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x279))
 			continue;
 
-		Common::Error lineError = playButcherLine(0x1a4e);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x1a4e, "BUTCHER", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
diff --git a/engines/harvester/npc/butcher_dialogue.h b/engines/harvester/npc/butcher_dialogue.h
index 9a63fd69e29..f6c1eec6b9c 100644
--- a/engines/harvester/npc/butcher_dialogue.h
+++ b/engines/harvester/npc/butcher_dialogue.h
@@ -36,7 +36,6 @@ public:
 		int currentTopicBufferLineIndex = -1;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = ButcherRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.introPending);
diff --git a/engines/harvester/npc/chessmaster_dialogue.cpp b/engines/harvester/npc/chessmaster_dialogue.cpp
index ad4a21c3002..96a47521eb2 100644
--- a/engines/harvester/npc/chessmaster_dialogue.cpp
+++ b/engines/harvester/npc/chessmaster_dialogue.cpp
@@ -35,7 +35,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kChessmasterNpc = "CHESSMASTER";
 static const char *const kChessSolvedFlag = "CHESS_SOLVED";
 static const char *const kChessKingMonsterActionTag = "ADD_CHESS_KING_MONSTER";
 static const int kChessmasterSlashDamageType = 2;
@@ -353,13 +352,9 @@ static Common::Error runChessmasterThreeMovePuzzle(DialogueRuntime &runtime, boo
 
 } // End of namespace
 
-bool ChessmasterDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kChessmasterNpc);
-}
-
 Common::Error ChessmasterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
-	Common::Error lineError = runtime.playDialogueLine(0x4115, kChessmasterNpc);
+	Common::Error lineError = runtime.playDialogueLine(0x4115, "CHESSMASTER");
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -370,7 +365,7 @@ Common::Error ChessmasterDialogueHandler::handleDialogue(DialogueRuntime &runtim
 
 	bool solvedPuzzle = false;
 	if (responseIndex == 1) {
-		lineError = runtime.playDialogueLineWithVariant(0x4122, kChessmasterNpc, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x4122, "CHESSMASTER", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -382,7 +377,7 @@ Common::Error ChessmasterDialogueHandler::handleDialogue(DialogueRuntime &runtim
 	}
 
 	if (responseIndex != 1 || !solvedPuzzle) {
-		lineError = runtime.playDialogueLineWithVariant(0x4126, kChessmasterNpc, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x4126, "CHESSMASTER", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -405,7 +400,7 @@ Common::Error ChessmasterDialogueHandler::handleDialogue(DialogueRuntime &runtim
 	InteractionResult interaction;
 	// C125.FST already shows the chessmaster death; do not queue the room death bank again.
 	if (runtime.startupScript().finalizeRuntimeNpcDeathOrMonsterfy(
-			kChessmasterNpc, kChessmasterSlashDamageType)) {
+			"CHESSMASTER", kChessmasterSlashDamageType)) {
 		interaction.mutatedRuntimeState = true;
 		interaction.visualRuntimeStateChanged = true;
 	}
diff --git a/engines/harvester/npc/chessmaster_dialogue.h b/engines/harvester/npc/chessmaster_dialogue.h
index 867e94d7453..faa7ff24021 100644
--- a/engines/harvester/npc/chessmaster_dialogue.h
+++ b/engines/harvester/npc/chessmaster_dialogue.h
@@ -28,7 +28,6 @@ namespace Harvester {
 
 class ChessmasterDialogueHandler : public NpcDialogueHandler {
 public:
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {}
 	Common::Error handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) override;
diff --git a/engines/harvester/npc/cloak_atnd_dialogue.cpp b/engines/harvester/npc/cloak_atnd_dialogue.cpp
index d8a6103cb67..ce89b927d66 100644
--- a/engines/harvester/npc/cloak_atnd_dialogue.cpp
+++ b/engines/harvester/npc/cloak_atnd_dialogue.cpp
@@ -28,7 +28,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kCloakAtndNpc = "CLOAK_ATND";
 static const char *const kCleanedClothesFlag = "CLEANED_CLOTHES";
 static const char *const kCleanClothesActionTag = "CLEAN_CLOTHES";
 static const char *const kPaymentItemName = "BARCASHFIVE";
@@ -36,10 +35,6 @@ static const int kNeedCleaningPaymentLine = 0x1066;
 
 } // End of namespace
 
-bool CloakAtndDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kCloakAtndNpc);
-}
-
 Common::Error CloakAtndDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &) {
 	if (!usedItemName.empty()) {
@@ -58,7 +53,7 @@ Common::Error CloakAtndDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue(kCleanedClothesFlag))
 		return Common::kNoError;
 
-	return runtime.playDialogueLine(kNeedCleaningPaymentLine, kCloakAtndNpc);
+	return runtime.playDialogueLine(kNeedCleaningPaymentLine, "CLOAK_ATND");
 }
 
 } // End of namespace Harvester
diff --git a/engines/harvester/npc/cloak_atnd_dialogue.h b/engines/harvester/npc/cloak_atnd_dialogue.h
index 44a749eef07..2a4bb404998 100644
--- a/engines/harvester/npc/cloak_atnd_dialogue.h
+++ b/engines/harvester/npc/cloak_atnd_dialogue.h
@@ -28,7 +28,6 @@ namespace Harvester {
 
 class CloakAtndDialogueHandler : public NpcDialogueHandler {
 public:
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {}
 	Common::Error handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) override;
diff --git a/engines/harvester/npc/curator_dialogue.cpp b/engines/harvester/npc/curator_dialogue.cpp
index 571809353aa..127b80cb5c6 100644
--- a/engines/harvester/npc/curator_dialogue.cpp
+++ b/engines/harvester/npc/curator_dialogue.cpp
@@ -28,26 +28,20 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kCuratorNpc = "CURATOR";
-static const char *const kPcSpeaker = "PC";
 static const int kCuratorResponseLine = 0x4a;
 
 static const DialogueLineEntry kCuratorTailLines[] = {
-	{ 0x43a, kCuratorNpc, 0 },
-	{ 0x43b, kCuratorNpc, 0 },
-	{ 0x447, kPcSpeaker, 0 },
-	{ 0x44b, kCuratorNpc, 3 }
+	{ 0x43a, "CURATOR", 0 },
+	{ 0x43b, "CURATOR", 0 },
+	{ 0x447, "PC", 0 },
+	{ 0x44b, "CURATOR", 3 }
 };
 
 } // End of namespace
 
-bool CuratorDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kCuratorNpc);
-}
-
 Common::Error CuratorDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
-	Common::Error lineError = runtime.playDialogueLineWithVariant(0x425, kCuratorNpc, 1);
+	Common::Error lineError = runtime.playDialogueLineWithVariant(0x425, "CURATOR", 1);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -57,9 +51,9 @@ Common::Error CuratorDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return responseError;
 
 	if (responseIndex == 1) {
-		lineError = runtime.playDialogueLineWithVariant(0x430, kCuratorNpc, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x430, "CURATOR", 3);
 	} else if (responseIndex == 2) {
-		lineError = runtime.playDialogueLineWithVariant(0x435, kCuratorNpc, 0);
+		lineError = runtime.playDialogueLineWithVariant(0x435, "CURATOR", 0);
 	} else {
 		lineError = Common::kNoError;
 	}
diff --git a/engines/harvester/npc/curator_dialogue.h b/engines/harvester/npc/curator_dialogue.h
index 89ad1b0f654..2a060f93a9d 100644
--- a/engines/harvester/npc/curator_dialogue.h
+++ b/engines/harvester/npc/curator_dialogue.h
@@ -28,7 +28,6 @@ namespace Harvester {
 
 class CuratorDialogueHandler : public NpcDialogueHandler {
 public:
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {}
 	Common::Error handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) override;
diff --git a/engines/harvester/npc/dad_dialogue.cpp b/engines/harvester/npc/dad_dialogue.cpp
index aead72a156c..a16063882f1 100644
--- a/engines/harvester/npc/dad_dialogue.cpp
+++ b/engines/harvester/npc/dad_dialogue.cpp
@@ -30,8 +30,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kDadNpc = "DAD";
-static const char *const kPcSpeaker = "PC";
 static const char *const kInventoryOwnerName = "INVENTORY";
 static const char *const kEnglishDadIntroResponseMenu = "1. I'd like that./2. I'd rather not.";
 static const int kFrenchDadIntroResponseMenuLineIndex = 0x328;
@@ -40,61 +38,61 @@ static const char *const kDialogueC112S4FstPath = "GRAPHIC/FST/C112S4.FST";
 static const char *const kDialogueC112S7FstPath = "GRAPHIC/FST/C112S7.FST";
 
 static const DialogueLineEntry kDadIntroLines[] = {
-	{ 0x39a5, kDadNpc, 0 },
-	{ 0x39a9, kPcSpeaker, 0 },
-	{ 0x39ad, kDadNpc, 0 },
-	{ 0x39b1, kPcSpeaker, 2 }
+	{ 0x39a5, "DAD", 0 },
+	{ 0x39a9, "PC", 0 },
+	{ 0x39ad, "DAD", 0 },
+	{ 0x39b1, "PC", 2 }
 };
 
 static const DialogueLineEntry kDadIntroContinuationLines[] = {
-	{ 0x39b5, kDadNpc, 0 },
-	{ 0x39ba, kPcSpeaker, 3 }
+	{ 0x39b5, "DAD", 0 },
+	{ 0x39ba, "PC", 3 }
 };
 
 static const DialogueLineEntry kDadOfferPromptLines[] = {
-	{ 0x39c2, kPcSpeaker, 2 },
-	{ 0x39c6, kDadNpc, 0 }
+	{ 0x39c2, "PC", 2 },
+	{ 0x39c6, "DAD", 0 }
 };
 
 static const DialogueLineEntry kDadOfferAcceptedLines[] = {
-	{ 0x39d3, kDadNpc, 0 },
-	{ 0x39d4, kDadNpc, 0 },
-	{ 0x39d5, kDadNpc, 0 },
-	{ 0x39d6, kDadNpc, 0 },
-	{ 0x39d7, kDadNpc, 0 }
+	{ 0x39d3, "DAD", 0 },
+	{ 0x39d4, "DAD", 0 },
+	{ 0x39d5, "DAD", 0 },
+	{ 0x39d6, "DAD", 0 },
+	{ 0x39d7, "DAD", 0 }
 };
 
 static const DialogueLineEntry kDadOfferClosingLines[] = {
-	{ 0x39e6, kPcSpeaker, 0 },
-	{ 0x39eb, kDadNpc, 0 }
+	{ 0x39e6, "PC", 0 },
+	{ 0x39eb, "DAD", 0 }
 };
 
 static const DialogueLineEntry kDadMeatPermissionUnavailableLines[] = {
-	{ 0x39fe, kPcSpeaker, 0 },
-	{ 0x3a02, kDadNpc, 0 }
+	{ 0x39fe, "PC", 0 },
+	{ 0x3a02, "DAD", 0 }
 };
 
 static const DialogueLineEntry kDadWhaleyPhotoLines[] = {
-	{ 0x3a41, kDadNpc, 0 },
-	{ 0x3a46, kPcSpeaker, 0 },
-	{ 0x3a4a, kDadNpc, 0 }
+	{ 0x3a41, "DAD", 0 },
+	{ 0x3a46, "PC", 0 },
+	{ 0x3a4a, "DAD", 0 }
 };
 
 static const DialogueLineEntry kDadBlackmailEvidenceLines[] = {
-	{ 0x3a51, kDadNpc, 0 },
-	{ 0x3a55, kPcSpeaker, 0 },
-	{ 0x3a59, kDadNpc, 0 }
+	{ 0x3a51, "DAD", 0 },
+	{ 0x3a55, "PC", 0 },
+	{ 0x3a59, "DAD", 0 }
 };
 
 static const DialogueLineEntry kDadMeatPermissionConversionLines[] = {
-	{ 0x39f2, kPcSpeaker, 0 },
-	{ 0x39f7, kDadNpc, 0 }
+	{ 0x39f2, "PC", 0 },
+	{ 0x39f7, "DAD", 0 }
 };
 
 static const DialogueLineEntry kDadLodgeTopicLines[] = {
-	{ 0x3a7d, kDadNpc, 0 },
-	{ 0x3a83, kPcSpeaker, 0 },
-	{ 0x3a88, kDadNpc, 0 }
+	{ 0x3a7d, "DAD", 0 },
+	{ 0x3a83, "PC", 0 },
+	{ 0x3a88, "DAD", 0 }
 };
 
 static Common::String getDadIntroResponseMenu(DialogueRuntime &runtime) {
@@ -117,22 +115,12 @@ static Common::String getDadIntroResponseMenu(DialogueRuntime &runtime) {
 
 } // End of namespace
 
-bool DadDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kDadNpc);
-}
-
 Common::Error DadDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	DadRoomDialogueState &state = _state;
-	auto playDadLine = [&](int wavId, const char *speakerId = kDadNpc,
-			int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, speakerId, headVariant);
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
+
 	auto playDadMeatPermissionConversion = [&]() -> Common::Error {
-		Common::Error lineError = playSequence(kDadMeatPermissionConversionLines,
+		Common::Error lineError = runtime.playDialogueEntrySequence(kDadMeatPermissionConversionLines,
 			ARRAYSIZE(kDadMeatPermissionConversionLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
@@ -158,7 +146,7 @@ Common::Error DadDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	};
 
 	if (runtime.startupScript().getFlagValue("TAKING_BONDAGE")) {
-		Common::Error lineError = playDadLine(0x3a61);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3a61, "DAD", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -166,33 +154,33 @@ Common::Error DadDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		return playDadLine(0x3a6b);
+		return runtime.playDialogueLineWithVariant(0x3a6b, "DAD", 0);
 	}
 
 	if (!usedItemName.empty()) {
 		if (usedItemName.equalsIgnoreCase("CASKET_PHOTO") ||
 				usedItemName.equalsIgnoreCase("CASKET_PHOTOCOPY")) {
-			return playDadLine(0x3a39);
+			return runtime.playDialogueLineWithVariant(0x3a39, "DAD", 0);
 		}
 
 		if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
 			sharedState.discussedWhaleyHerrillPhoto = 1;
-			return playSequence(kDadWhaleyPhotoLines, ARRAYSIZE(kDadWhaleyPhotoLines));
+			return runtime.playDialogueEntrySequence(kDadWhaleyPhotoLines, ARRAYSIZE(kDadWhaleyPhotoLines));
 		}
 
 		if (isBlackmailEvidence(usedItemName)) {
 			sharedState.discussedNoteCheckbookEvidence = 1;
-			return playSequence(kDadBlackmailEvidenceLines, ARRAYSIZE(kDadBlackmailEvidenceLines));
+			return runtime.playDialogueEntrySequence(kDadBlackmailEvidenceLines, ARRAYSIZE(kDadBlackmailEvidenceLines));
 		}
 
 		if (usedItemName.equalsIgnoreCase("MEAT_PERMISSION0")) {
 			if (sharedState.dadMeatPermissionState != 0)
 				return playDadMeatPermissionConversion();
 
-			return playDadLine(0x3a32);
+			return runtime.playDialogueLineWithVariant(0x3a32, "DAD", 0);
 		}
 
-		return playDadLine(0x3a32);
+		return runtime.playDialogueLineWithVariant(0x3a32, "DAD", 0);
 	}
 
 	Common::Error lineError = Common::kNoError;
@@ -200,13 +188,12 @@ Common::Error DadDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		sharedState.dialogueStateD2ed0 = true;
 		state.introPending = false;
 
-		lineError = playSequence(kDadIntroLines, ARRAYSIZE(kDadIntroLines));
+		lineError = runtime.playDialogueEntrySequence(kDadIntroLines, ARRAYSIZE(kDadIntroLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
 		sharedState.dialogueStateD2f04 = true;
-		lineError = playSequence(kDadIntroContinuationLines,
-			ARRAYSIZE(kDadIntroContinuationLines));
+		lineError = runtime.playDialogueEntrySequence(kDadIntroContinuationLines, ARRAYSIZE(kDadIntroContinuationLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -214,7 +201,7 @@ Common::Error DadDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playSequence(kDadOfferPromptLines, ARRAYSIZE(kDadOfferPromptLines));
+		lineError = runtime.playDialogueEntrySequence(kDadOfferPromptLines, ARRAYSIZE(kDadOfferPromptLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -226,17 +213,16 @@ Common::Error DadDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playSequence(kDadOfferAcceptedLines,
-				ARRAYSIZE(kDadOfferAcceptedLines));
+			lineError = runtime.playDialogueEntrySequence(kDadOfferAcceptedLines, ARRAYSIZE(kDadOfferAcceptedLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else if (responseIndex == 2) {
-			lineError = playDadLine(0x39e0);
+			lineError = runtime.playDialogueLineWithVariant(0x39e0, "DAD", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 
-		lineError = playSequence(kDadOfferClosingLines, ARRAYSIZE(kDadOfferClosingLines));
+		lineError = runtime.playDialogueEntrySequence(kDadOfferClosingLines, ARRAYSIZE(kDadOfferClosingLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -245,7 +231,7 @@ Common::Error DadDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else {
-			lineError = playSequence(kDadMeatPermissionUnavailableLines,
+			lineError = runtime.playDialogueEntrySequence(kDadMeatPermissionUnavailableLines,
 				ARRAYSIZE(kDadMeatPermissionUnavailableLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -254,12 +240,12 @@ Common::Error DadDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			lineError = playDadLine(0x3a0f);
+			lineError = runtime.playDialogueLineWithVariant(0x3a0f, "DAD", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 	} else {
-		lineError = playDadLine(0x3a17);
+		lineError = runtime.playDialogueLineWithVariant(0x3a17, "DAD", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -281,9 +267,9 @@ Common::Error DadDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (isLodgeTopic(selectedTopic)) {
 			sharedState.discussedLodgeTopic = true;
-			lineError = playSequence(kDadLodgeTopicLines, ARRAYSIZE(kDadLodgeTopicLines));
+			lineError = runtime.playDialogueEntrySequence(kDadLodgeTopicLines, ARRAYSIZE(kDadLodgeTopicLines));
 		} else {
-			lineError = playDadLine(0x3a2c);
+			lineError = runtime.playDialogueLineWithVariant(0x3a2c, "DAD", 0);
 		}
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
diff --git a/engines/harvester/npc/dad_dialogue.h b/engines/harvester/npc/dad_dialogue.h
index 5b5a523fdf5..a058ed8c6f9 100644
--- a/engines/harvester/npc/dad_dialogue.h
+++ b/engines/harvester/npc/dad_dialogue.h
@@ -33,7 +33,6 @@ public:
 		bool meatPermissionState = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = DadRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.introPending);
diff --git a/engines/harvester/npc/dark_woman_dialogue.cpp b/engines/harvester/npc/dark_woman_dialogue.cpp
index 87683429d43..5db88eb2139 100644
--- a/engines/harvester/npc/dark_woman_dialogue.cpp
+++ b/engines/harvester/npc/dark_woman_dialogue.cpp
@@ -28,7 +28,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kDarkWomanNpc = "DARK_WOMAN";
 static const char *const kHandMirrorItemName = "HANDMIRROR";
 static const char *const kInventoryOwnerName = "INVENTORY";
 static const char *const kSetDarkWomanFlagActionTag = "SET_D_W_FLAG";
@@ -41,25 +40,18 @@ static const int kDarkWomanNeedsMirrorResponseLine = 0x4f;
 
 } // End of namespace
 
-bool DarkWomanDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kDarkWomanNpc);
-}
-
 Common::Error DarkWomanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &) {
-	auto playDarkWomanLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kDarkWomanNpc, headVariant);
-	};
 	auto queueDarkWomanMonsterfyTransition = [&]() {
 		InteractionResult interaction;
-		if (runtime.startupScript().queueRuntimeNpcDeathOrMonsterfy(kDarkWomanNpc)) {
+		if (runtime.startupScript().queueRuntimeNpcDeathOrMonsterfy("DARK_WOMAN")) {
 			interaction.mutatedRuntimeState = true;
 			interaction.visualRuntimeStateChanged = true;
 		}
 		runtime.queueDialogueInteractionIfNeeded(interaction);
 	};
 	auto runDarkWomanMonsterfyBranch = [&]() -> Common::Error {
-		Common::Error lineError = playDarkWomanLine(0x4ce2, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x4ce2, "DARK_WOMAN", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -67,7 +59,7 @@ Common::Error DarkWomanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return Common::kNoError;
 	};
 	auto runMirrorRevealBranch = [&]() -> Common::Error {
-		Common::Error lineError = playDarkWomanLine(0x4d03, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x4d03, "DARK_WOMAN", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -75,7 +67,7 @@ Common::Error DarkWomanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playDarkWomanLine(0x4d0d, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x4d0d, "DARK_WOMAN", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -100,7 +92,7 @@ Common::Error DarkWomanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	_state.talkStatePending = false;
 
-	Common::Error lineError = playDarkWomanLine(0x4cd5, 1);
+	Common::Error lineError = runtime.playDialogueLineWithVariant(0x4cd5, "DARK_WOMAN", 1);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -115,7 +107,7 @@ Common::Error DarkWomanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (responseIndex != 2)
 		return Common::kNoError;
 
-	lineError = playDarkWomanLine(0x4ce6, 4);
+	lineError = runtime.playDialogueLineWithVariant(0x4ce6, "DARK_WOMAN", 4);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -130,7 +122,7 @@ Common::Error DarkWomanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (responseIndex != 2)
 		return Common::kNoError;
 
-	lineError = playDarkWomanLine(0x4ce6, 4);
+	lineError = runtime.playDialogueLineWithVariant(0x4ce6, "DARK_WOMAN", 4);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -145,7 +137,7 @@ Common::Error DarkWomanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (responseIndex != 2)
 		return Common::kNoError;
 
-	lineError = playDarkWomanLine(0x4ce6, 4);
+	lineError = runtime.playDialogueLineWithVariant(0x4ce6, "DARK_WOMAN", 4);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -160,7 +152,7 @@ Common::Error DarkWomanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (responseIndex == 1)
 		return runDarkWomanMonsterfyBranch();
 	if (!hasHandMirror)
-		return playDarkWomanLine(0x4cfc, 2);
+		return runtime.playDialogueLineWithVariant(0x4cfc, "DARK_WOMAN", 2);
 
 	return runMirrorRevealBranch();
 }
diff --git a/engines/harvester/npc/dark_woman_dialogue.h b/engines/harvester/npc/dark_woman_dialogue.h
index 9351d9c15c6..eb3e3e00c6b 100644
--- a/engines/harvester/npc/dark_woman_dialogue.h
+++ b/engines/harvester/npc/dark_woman_dialogue.h
@@ -32,7 +32,6 @@ public:
 		bool talkStatePending = true;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = DarkWomanRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.talkStatePending);
diff --git a/engines/harvester/npc/dialogue_handler.h b/engines/harvester/npc/dialogue_handler.h
index d3c45f829ad..7877b7f7bbd 100644
--- a/engines/harvester/npc/dialogue_handler.h
+++ b/engines/harvester/npc/dialogue_handler.h
@@ -100,7 +100,6 @@ class NpcDialogueHandler {
 public:
 	virtual ~NpcDialogueHandler() {}
 
-	virtual bool matchesNpc(const Common::String &npcName) const = 0;
 	virtual void resetState() {}
 	virtual void syncState(Common::Serializer &) {}
 	virtual void migrateSharedState(DialogueSharedState &) {}
diff --git a/engines/harvester/npc/dialogue_runtime.h b/engines/harvester/npc/dialogue_runtime.h
index e04c53ab5f9..50b7ce3cfa2 100644
--- a/engines/harvester/npc/dialogue_runtime.h
+++ b/engines/harvester/npc/dialogue_runtime.h
@@ -78,6 +78,13 @@ public:
 			bool allowTransitions = true) const {
 		return _script.executeActionTag(tag, result, allowTransitions, _currentRoomName);
 	}
+	void executeAndQueueAction(const Common::String &tag) const {
+		InteractionResult interaction;
+		if (executeActionTag(tag, interaction)) {
+			applyImmediateDialogueInteractionEffects(interaction);
+			queueDialogueInteractionIfNeeded(interaction);
+		}
+	}
 
 	Common::Error playDialogueLineWithVariant(int wavId, const Common::String &speakerId,
 			int headVariant) const {
@@ -139,6 +146,27 @@ private:
 	mutable KeywordMenuSelectionState _lastKeywordSelection;
 };
 
+class DialogueTopicBuffer {
+public:
+	DialogueTopicBuffer(const DialogueRuntime &runtime, Common::String &text, int &lineIndex,
+			const char *label)
+		: _runtime(runtime), _text(text), _lineIndex(lineIndex), _label(label) {}
+
+	void assign(int responseLineIndex) const {
+		_runtime.assignTopicBuffer(_text, _lineIndex, responseLineIndex, _label);
+	}
+	void clear() const {
+		_text.clear();
+		_lineIndex = -1;
+	}
+
+private:
+	const DialogueRuntime &_runtime;
+	Common::String &_text;
+	int &_lineIndex;
+	const char *_label;
+};
+
 } // End of namespace Harvester
 
 #endif // HARVESTER_NPC_DIALOGUE_RUNTIME_H
diff --git a/engines/harvester/npc/dwayne_dialogue.cpp b/engines/harvester/npc/dwayne_dialogue.cpp
index e2e465a3bbe..8df403f2f11 100644
--- a/engines/harvester/npc/dwayne_dialogue.cpp
+++ b/engines/harvester/npc/dwayne_dialogue.cpp
@@ -30,11 +30,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kDwayneNpc = "DWAYNE";
-static const char *const kDwayneDnaLeftNpc = "DWAYNE_DNALFT";
-static const char *const kDwayneStBedroomNpc = "DWAYNE_ST_BEDRM";
-static const char *const kEdnaNpc = "EDNA";
-static const char *const kPcSpeaker = "PC";
 static const char *const kInventoryOwnerName = "INVENTORY";
 static const char *const kRahRoomName = "RAH";
 
@@ -43,12 +38,6 @@ static const int kDwayneLoomisTopicResponseLines[] = { 0x8c, 0x8d };
 
 } // End of namespace
 
-bool DwayneDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kDwayneNpc) ||
-		npcName.equalsIgnoreCase(kDwayneDnaLeftNpc) ||
-		npcName.equalsIgnoreCase(kDwayneStBedroomNpc);
-}
-
 Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	DwayneRoomDialogueState &state = _state;
@@ -57,19 +46,8 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	int &dwayneTopicBufferLineIndex = state.currentTopicBufferLineIndex;
 	state.currentTopicBuffer.clear();
 	state.currentTopicBufferLineIndex = -1;
-	auto assignDwayneTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(dwayneTopicBuffer, dwayneTopicBufferLineIndex,
-			responseLineIndex, "Dwayne topic buffer");
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
-	auto playDwayneLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kDwayneNpc, headVariant);
-	};
-	auto playEdnaLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kEdnaNpc, headVariant);
-	};
+	DialogueTopicBuffer topics(runtime, dwayneTopicBuffer, dwayneTopicBufferLineIndex, "Dwayne topic buffer");
+
 	auto hideObjectIfPresent = [&](const char *ownerOrRoom, const Common::String &objectName) {
 		if (startupScript.isObjectInInventory(objectName))
 			(void)startupScript.setRuntimeObjectVisible(ownerOrRoom, objectName, false);
@@ -86,14 +64,7 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		(void)startupScript.addRuntimeObjectToInventory("GOOJF_CARD");
 		(void)startupScript.setRuntimeFlagValue("PC_HAS_GOOJF_CARD", true);
 	};
-	auto executeActionTagIfSet = [&](const char *tag) {
-		InteractionResult interaction;
-		if (!runtime.executeActionTag(tag, interaction))
-			return;
 
-		runtime.applyImmediateDialogueInteractionEffects(interaction);
-		runtime.queueDialogueInteractionIfNeeded(interaction);
-	};
 	auto setDwayneIntroduced = [&]() {
 		(void)startupScript.setRuntimeFlagValue("DWAYNE_INTRODUCED", true);
 	};
@@ -113,44 +84,44 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		(void)startupScript.setRuntimeFlagValue("ARREST_FLAG", false);
 
 		if (startupScript.getFlagValue("PC_BUSTED_KILLED_MOYNAHAN")) {
-			Common::Error lineError = playDwayneLine(0x38d6, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x38d6, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			(void)startupScript.setRuntimeFlagValue("PC_BUSTED_KILLED_MOYNAHAN", false);
 		} else if (startupScript.getFlagValue("PC_BUSTED_KILLED_JIMMY")) {
-			Common::Error lineError = playDwayneLine(0x384d, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x384d, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			(void)startupScript.setRuntimeFlagValue("PC_BUSTED_KILLED_JIMMY", false);
 			(void)startupScript.setRuntimeFlagValue("KILLED_JIMMY", false);
 		} else if (startupScript.getFlagValue("PC_BUSTED_KILLED_JOHNSON")) {
-			Common::Error lineError = playDwayneLine(0x386d, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x386d, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			(void)startupScript.setRuntimeFlagValue("PC_BUSTED_KILLED_JOHNSON", false);
 		} else if (startupScript.getFlagValue("PC_BUSTED_KILLED_PHELPS")) {
-			Common::Error lineError = playDwayneLine(0x38bc, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x38bc, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			(void)startupScript.setRuntimeFlagValue("PC_BUSTED_KILLED_PHELPS", false);
 		} else if (startupScript.getFlagValue("PC_BUSTED_KILLED_OREILLY")) {
-			Common::Error lineError = playDwayneLine(0x38b5, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x38b5, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			(void)startupScript.setRuntimeFlagValue("PC_BUSTED_KILLED_OREILLY", false);
 			(void)startupScript.setRuntimeFlagValue("KILLED_OREILLY", false);
 		} else if (startupScript.getFlagValue("IF_KILL_POTTSDAM_AT_GRAVE")) {
-			Common::Error lineError = playDwayneLine(0x37ed, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x37ed, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			(void)startupScript.setRuntimeFlagValue("IF_KILL_POTTSDAM_AT_GRAVE", false);
 		} else if (startupScript.getFlagValue("GENERIC_BUST")) {
-			Common::Error lineError = playDwayneLine(0x38fc, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x38fc, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			(void)startupScript.setRuntimeFlagValue("GENERIC_BUST", false);
 		} else if (startupScript.getFlagValue("DNALFT_PERVERT")) {
-			Common::Error lineError = playDwayneLine(0x3580, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x3580, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			(void)startupScript.setRuntimeFlagValue("DNALFT_PERVERT", false);
@@ -158,17 +129,17 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (startupScript.getFlagValue("PC_HAS_GOOJF_CARD") &&
 				!startupScript.getFlagValue("PC_FRY_IN_CHAIR")) {
-			Common::Error lineError = playDwayneLine(0x3814);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x3814, "DWAYNE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			if (startupScript.getFlagValue("PC_KILLED_ANYONE")) {
 				const DialogueLineEntry goojfCardLines[] = {
-					{ 0x3820, kDwayneNpc, 0 },
-					{ 0x3824, kPcSpeaker, 0 },
-					{ 0x3828, kDwayneNpc, 0 }
+					{ 0x3820, "DWAYNE", 0 },
+					{ 0x3824, "PC", 0 },
+					{ 0x3828, "DWAYNE", 0 }
 				};
-				lineError = playSequence(goojfCardLines, ARRAYSIZE(goojfCardLines));
+				lineError = runtime.playDialogueEntrySequence(goojfCardLines, ARRAYSIZE(goojfCardLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -177,15 +148,15 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				if (responseError.getCode() != Common::kNoError)
 					return responseError;
 				if (responseIndex == 1) {
-					lineError = playDwayneLine(0x3832, 1);
+					lineError = runtime.playDialogueLineWithVariant(0x3832, "DWAYNE", 1);
 				} else if (responseIndex == 2) {
-					lineError = playDwayneLine(0x383b, 2);
+					lineError = runtime.playDialogueLineWithVariant(0x383b, "DWAYNE", 2);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
-					lineError = runtime.playDialogueLineWithVariant(0x3840, kPcSpeaker, 0);
+					lineError = runtime.playDialogueLineWithVariant(0x3840, "PC", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
-					lineError = playDwayneLine(0x3844, 2);
+					lineError = runtime.playDialogueLineWithVariant(0x3844, "DWAYNE", 2);
 				}
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
@@ -198,32 +169,32 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (startupScript.getFlagValue("PC_FRY_IN_CHAIR"))
 			return Common::kNoError;
 		if (startupScript.getFlagValue("BUSTED_THIRD"))
-			return playDwayneLine(0x3928, 2);
+			return runtime.playDialogueLineWithVariant(0x3928, "DWAYNE", 2);
 		if (startupScript.getFlagValue("BUSTED_TWICE"))
-			return playDwayneLine(0x3921, 2);
+			return runtime.playDialogueLineWithVariant(0x3921, "DWAYNE", 2);
 		if (startupScript.getFlagValue("BUSTED_ONCE"))
-			return playDwayneLine(0x3918);
+			return runtime.playDialogueLineWithVariant(0x3918, "DWAYNE", 0);
 		return Common::kNoError;
 	}
 
 	if (startupScript.getFlagValue("CHECK_EVIDENCE_DOOR")) {
-		Common::Error lineError = playDwayneLine(0x3386);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3386, "DWAYNE", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		(void)startupScript.setRuntimeFlagValue("CHECK_EVIDENCE_DOOR", false);
 		return Common::kNoError;
 	}
 	if (startupScript.getFlagValue("PC_TRIES_TO_TAKE_STEPHANIES_REMAINS"))
-		return playDwayneLine(0x34e3, 2);
+		return runtime.playDialogueLineWithVariant(0x34e3, "DWAYNE", 2);
 	if (runtime.currentRoomName().equalsIgnoreCase("ST_BEDRM")) {
 		if (!startupScript.getFlagValue("SD_TALKED_OF_CARD")) {
-			Common::Error lineError = playDwayneLine(0x34da);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x34da, "DWAYNE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			(void)startupScript.setRuntimeFlagValue("SD_TALKED_OF_CARD", true);
 			return Common::kNoError;
 		}
-		return playDwayneLine(0x3395);
+		return runtime.playDialogueLineWithVariant(0x3395, "DWAYNE", 0);
 	}
 
 	if (startupScript.getFlagValue("BRING_KARIN_TO_SHERIFF") &&
@@ -232,12 +203,12 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		setPendingKarinAliveFollowup();
 		state.bringKarinToSheriffLinePlayed = true;
 		const DialogueLineEntry lines[] = {
-			{ 0x3750, kDwayneNpc, 1 },
-			{ 0x3754, kPcSpeaker, 0 },
-			{ 0x3759, kDwayneNpc, 0 },
-			{ 0x375d, kDwayneNpc, 1 }
+			{ 0x3750, "DWAYNE", 1 },
+			{ 0x3754, "PC", 0 },
+			{ 0x3759, "DWAYNE", 0 },
+			{ 0x375d, "DWAYNE", 1 }
 		};
-		return playSequence(lines, ARRAYSIZE(lines));
+		return runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 	}
 	if (startupScript.getFlagValue("KARIN_FOUND_ALIVE"))
 		setPendingKarinAliveFollowup();
@@ -247,30 +218,30 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			state.discussedBoylesButton = true;
 			sharedState.dwayneDiscussedBoylesButton = true;
 			const DialogueLineEntry lines[] = {
-				{ 0x339b, kDwayneNpc, 0 },
-				{ 0x33a0, kPcSpeaker, 0 },
-				{ 0x33a5, kDwayneNpc, 0 },
-				{ 0x33a9, kPcSpeaker, 0 },
-				{ 0x33ad, kDwayneNpc, 2 }
+				{ 0x339b, "DWAYNE", 0 },
+				{ 0x33a0, "PC", 0 },
+				{ 0x33a5, "DWAYNE", 0 },
+				{ 0x33a9, "PC", 0 },
+				{ 0x33ad, "DWAYNE", 2 }
 			};
-			return playSequence(lines, ARRAYSIZE(lines));
+			return runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 		}
 		if (usedItemName.equalsIgnoreCase("K_PURSE")) {
 			state.discussedKarinPurse = true;
 			sharedState.discussedKarinPurse = 1;
 			const DialogueLineEntry lines[] = {
-				{ 0x37cb, kDwayneNpc, 1 },
-				{ 0x37cf, kPcSpeaker, 3 },
-				{ 0x37b4, kDwayneNpc, 3 },
-				{ 0x37c1, kDwayneNpc, 0 },
-				{ 0x37c5, kPcSpeaker, 0 }
+				{ 0x37cb, "DWAYNE", 1 },
+				{ 0x37cf, "PC", 3 },
+				{ 0x37b4, "DWAYNE", 3 },
+				{ 0x37c1, "DWAYNE", 0 },
+				{ 0x37c5, "PC", 0 }
 			};
-			return playSequence(lines, ARRAYSIZE(lines));
+			return runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 		}
 		if (usedItemName.equalsIgnoreCase("CHECKBOOK_PHOTOCOPY") ||
 				usedItemName.equalsIgnoreCase("NOTE_PHOTOCOPY")) {
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownEvidenceOfBlackmail, true);
-			Common::Error lineError = playDwayneLine(0x33b9, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x33b9, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -279,17 +250,17 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1) {
-				lineError = playDwayneLine(0x33c3, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x33c3, "DWAYNE", 2);
 			} else if (responseIndex == 2) {
-				lineError = playDwayneLine(0x33c7, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x33c7, "DWAYNE", 2);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			lineError = runtime.playDialogueLineWithVariant(0x33cb, kPcSpeaker, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x33cb, "PC", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playDwayneLine(0x33d2);
+			lineError = runtime.playDialogueLineWithVariant(0x33d2, "DWAYNE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -298,15 +269,15 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1) {
-				lineError = playDwayneLine(0x33e0);
+				lineError = runtime.playDialogueLineWithVariant(0x33e0, "DWAYNE", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				state.presentedEvidenceReplyOverride = true;
 			} else if (responseIndex == 2) {
-				lineError = playDwayneLine(0x33e7, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x33e7, "DWAYNE", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				executeActionTagIfSet("PC_FRY_DADDY_1");
+				runtime.executeAndQueueAction("PC_FRY_DADDY_1");
 			}
 			return Common::kNoError;
 		}
@@ -315,11 +286,11 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownEvidenceOfBlackmail, true);
 			if (state.presentedEvidenceReplyOverride) {
 				const DialogueLineEntry introLines[] = {
-					{ 0x342c, kDwayneNpc, 0 },
-					{ 0x3430, kPcSpeaker, 0 },
-					{ 0x3435, kDwayneNpc, 2 }
+					{ 0x342c, "DWAYNE", 0 },
+					{ 0x3430, "PC", 0 },
+					{ 0x3435, "DWAYNE", 2 }
 				};
-				Common::Error lineError = playSequence(introLines, ARRAYSIZE(introLines));
+				Common::Error lineError = runtime.playDialogueEntrySequence(introLines, ARRAYSIZE(introLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -328,14 +299,14 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				if (responseError.getCode() != Common::kNoError)
 					return responseError;
 				if (responseIndex == 1) {
-					lineError = playDwayneLine(0x3440, 2);
+					lineError = runtime.playDialogueLineWithVariant(0x3440, "DWAYNE", 2);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
-					executeActionTagIfSet("PC_FRY_DADDY_1");
+					runtime.executeAndQueueAction("PC_FRY_DADDY_1");
 					return Common::kNoError;
 				}
 				if (responseIndex == 2) {
-					lineError = playDwayneLine(0x3448);
+					lineError = runtime.playDialogueLineWithVariant(0x3448, "DWAYNE", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 					confiscateSheriffEvidence();
@@ -344,7 +315,7 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return Common::kNoError;
 			}
 
-			Common::Error lineError = playDwayneLine(0x33f4, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x33f4, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -354,17 +325,17 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 			lineError = Common::kNoError;
 			if (responseIndex == 1) {
-				lineError = playDwayneLine(0x33c3, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x33c3, "DWAYNE", 2);
 			} else if (responseIndex == 2) {
-				lineError = playDwayneLine(0x3403, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x3403, "DWAYNE", 2);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			lineError = runtime.playDialogueLineWithVariant(0x3407, kPcSpeaker, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x3407, "PC", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playDwayneLine(0x340d);
+			lineError = runtime.playDialogueLineWithVariant(0x340d, "DWAYNE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -375,17 +346,17 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseIndex == 0)
 				return Common::kNoError;
 			if (responseIndex == 1) {
-				lineError = playDwayneLine(0x341a);
+				lineError = runtime.playDialogueLineWithVariant(0x341a, "DWAYNE", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				executeActionTagIfSet("PC_FRY_DADDY_1");
+				runtime.executeAndQueueAction("PC_FRY_DADDY_1");
 				return Common::kNoError;
 			}
 			if (responseIndex == 2) {
-				lineError = playDwayneLine(0x3422, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x3422, "DWAYNE", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				executeActionTagIfSet("PC_FRY_DADDY_1");
+				runtime.executeAndQueueAction("PC_FRY_DADDY_1");
 				return Common::kNoError;
 			}
 			return Common::kNoError;
@@ -396,65 +367,65 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			sharedState.discussedLedgerEvidence = 1;
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownLedgersToAnyone, true);
 			const DialogueLineEntry lines[] = {
-				{ 0x3452, kDwayneNpc, 0 },
-				{ 0x3457, kPcSpeaker, 0 },
-				{ 0x345c, kDwayneNpc, 0 },
-				{ 0x3460, kPcSpeaker, 2 },
-				{ 0x3464, kDwayneNpc, 0 },
-				{ 0x346a, kPcSpeaker, 2 },
-				{ 0x346e, kDwayneNpc, 0 },
-				{ 0x3474, kPcSpeaker, 4 },
-				{ 0x3478, kDwayneNpc, 2 }
+				{ 0x3452, "DWAYNE", 0 },
+				{ 0x3457, "PC", 0 },
+				{ 0x345c, "DWAYNE", 0 },
+				{ 0x3460, "PC", 2 },
+				{ 0x3464, "DWAYNE", 0 },
+				{ 0x346a, "PC", 2 },
+				{ 0x346e, "DWAYNE", 0 },
+				{ 0x3474, "PC", 4 },
+				{ 0x3478, "DWAYNE", 2 }
 			};
-			Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			(void)startupScript.resetRuntimeObjectToInitialState("LEDGER");
 			(void)startupScript.resetRuntimeObjectToInitialState("LEDGER2");
-			executeActionTagIfSet(sheriffInDiner ? "CALL_POLICE" : "LEDGER_BUST");
+			runtime.executeAndQueueAction(sheriffInDiner ? "CALL_POLICE" : "LEDGER_BUST");
 			return Common::kNoError;
 		}
 		if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
 			sharedState.discussedWhaleyHerrillPhoto = 1;
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownPhotoOfWhaleyHerrill, true);
 			const DialogueLineEntry lines[] = {
-				{ 0x3975, kDwayneNpc, 3 },
-				{ 0x3979, kPcSpeaker, 0 },
-				{ 0x397e, kDwayneNpc, 0 }
+				{ 0x3975, "DWAYNE", 3 },
+				{ 0x3979, "PC", 0 },
+				{ 0x397e, "DWAYNE", 0 }
 			};
-			return playSequence(lines, ARRAYSIZE(lines));
+			return runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 		}
 		if (usedItemName.equalsIgnoreCase("CASKET_PHOTO") ||
 				usedItemName.equalsIgnoreCase("CASKET_PHOTOCOPY")) {
 			sharedState.discussedCasketPhotoEvidence = 1;
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownPhotoOfCorpse, true);
 
-			Common::Error lineError = playDwayneLine(0x3942, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x3942, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			if (sharedState.dialogueStateD2ec8) {
 				const DialogueLineEntry lines[] = {
-					{ 0x3949, kPcSpeaker, 0 },
-					{ 0x3950, kDwayneNpc, 0 },
-					{ 0x3955, kPcSpeaker, 4 },
-					{ 0x3959, kDwayneNpc, 1 },
-					{ 0x396d, kDwayneNpc, 0 }
+					{ 0x3949, "PC", 0 },
+					{ 0x3950, "DWAYNE", 0 },
+					{ 0x3955, "PC", 4 },
+					{ 0x3959, "DWAYNE", 1 },
+					{ 0x396d, "DWAYNE", 0 }
 				};
-				lineError = playSequence(lines, ARRAYSIZE(lines));
+				lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
 				hideObjectIfPresent(kInventoryOwnerName, usedItemName);
-				executeActionTagIfSet("CALL_POLICE");
+				runtime.executeAndQueueAction("CALL_POLICE");
 			} else {
 				const DialogueLineEntry lines[] = {
-					{ 0x3961, kPcSpeaker, 0 },
-					{ 0x3967, kDwayneNpc, 0 },
-					{ 0x396d, kDwayneNpc, 0 }
+					{ 0x3961, "PC", 0 },
+					{ 0x3967, "DWAYNE", 0 },
+					{ 0x396d, "DWAYNE", 0 }
 				};
-				lineError = playSequence(lines, ARRAYSIZE(lines));
+				lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -466,11 +437,11 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownEvidenceSheriffOwns, true);
 
 			const DialogueLineEntry introLines[] = {
-				{ 0x3686, kDwayneNpc, 0 },
-				{ 0x368a, kPcSpeaker, 0 },
-				{ 0x3690, kDwayneNpc, 0 }
+				{ 0x3686, "DWAYNE", 0 },
+				{ 0x368a, "PC", 0 },
+				{ 0x3690, "DWAYNE", 0 }
 			};
-			Common::Error lineError = playSequence(introLines, ARRAYSIZE(introLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(introLines, ARRAYSIZE(introLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -479,14 +450,14 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1) {
-				lineError = playDwayneLine(0x369b, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x369b, "DWAYNE", 2);
 			} else if (responseIndex == 2) {
 				const DialogueLineEntry responseLines[] = {
-					{ 0x36a3, kDwayneNpc, 0 },
-					{ 0x36a8, kPcSpeaker, 2 },
-					{ 0x36ac, kDwayneNpc, 2 }
+					{ 0x36a3, "DWAYNE", 0 },
+					{ 0x36a8, "PC", 2 },
+					{ 0x36ac, "DWAYNE", 2 }
 				};
-				lineError = playSequence(responseLines, ARRAYSIZE(responseLines));
+				lineError = runtime.playDialogueEntrySequence(responseLines, ARRAYSIZE(responseLines));
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -497,12 +468,12 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 		if (!usedItemName.equalsIgnoreCase("TV_DEED") &&
 				!usedItemName.equalsIgnoreCase("TV_DEED_PHOTOCOPY")) {
-			return playDwayneLine(0x3935);
+			return runtime.playDialogueLineWithVariant(0x3935, "DWAYNE", 0);
 		}
 
 		(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownEvidenceSheriffOwns, true);
 
-		Common::Error lineError = playDwayneLine(0x3608);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3608, "DWAYNE", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -511,14 +482,14 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (responseError.getCode() != Common::kNoError)
 			return responseError;
 		if (responseIndex == 1) {
-			lineError = playDwayneLine(0x3613);
+			lineError = runtime.playDialogueLineWithVariant(0x3613, "DWAYNE", 0);
 		} else if (responseIndex == 2) {
-			lineError = playDwayneLine(0x3618);
+			lineError = runtime.playDialogueLineWithVariant(0x3618, "DWAYNE", 0);
 		}
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playDwayneLine(0x361c);
+		lineError = runtime.playDialogueLineWithVariant(0x361c, "DWAYNE", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -527,9 +498,9 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (responseError.getCode() != Common::kNoError)
 			return responseError;
 		if (responseIndex == 1) {
-			lineError = playDwayneLine(0x3629, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x3629, "DWAYNE", 2);
 		} else if (responseIndex == 2) {
-			lineError = playDwayneLine(0x362d);
+			lineError = runtime.playDialogueLineWithVariant(0x362d, "DWAYNE", 0);
 		}
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
@@ -540,18 +511,18 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 		if (usedItemName.equalsIgnoreCase("TV_DEED")) {
 			const DialogueLineEntry deedLines[] = {
-				{ 0x363b, kDwayneNpc, 0 },
-				{ 0x3641, kPcSpeaker, 0 },
-				{ 0x3645, kDwayneNpc, 0 }
+				{ 0x363b, "DWAYNE", 0 },
+				{ 0x3641, "PC", 0 },
+				{ 0x3645, "DWAYNE", 0 }
 			};
-			lineError = playSequence(deedLines, ARRAYSIZE(deedLines));
+			lineError = runtime.playDialogueEntrySequence(deedLines, ARRAYSIZE(deedLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			executeActionTagIfSet("PC_FRY_DADDY_1");
+			runtime.executeAndQueueAction("PC_FRY_DADDY_1");
 			return Common::kNoError;
 		}
 		if (responseIndex != 1) {
-			lineError = playDwayneLine(0x367b, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x367b, "DWAYNE", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			hideObjectIfPresent(kRahRoomName, "TV_DEED_PHOTOCOPY");
@@ -559,11 +530,11 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 
 		const DialogueLineEntry copyLines[] = {
-			{ 0x364e, kDwayneNpc, 0 },
-			{ 0x3656, kPcSpeaker, 0 },
-			{ 0x365b, kDwayneNpc, 0 }
+			{ 0x364e, "DWAYNE", 0 },
+			{ 0x3656, "PC", 0 },
+			{ 0x365b, "DWAYNE", 0 }
 		};
-		lineError = playSequence(copyLines, ARRAYSIZE(copyLines));
+		lineError = runtime.playDialogueEntrySequence(copyLines, ARRAYSIZE(copyLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -572,7 +543,7 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (responseError.getCode() != Common::kNoError)
 			return responseError;
 		if (responseIndex == 1) {
-			lineError = playDwayneLine(0x3665);
+			lineError = runtime.playDialogueLineWithVariant(0x3665, "DWAYNE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			state.tvDeedReplyOverride = true;
@@ -580,14 +551,14 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 		if (responseIndex == 2) {
 			const DialogueLineEntry responseLines[] = {
-				{ 0x366b, kDwayneNpc, 3 },
-				{ 0x3670, kPcSpeaker, 2 },
-				{ 0x3674, kDwayneNpc, 2 }
+				{ 0x366b, "DWAYNE", 3 },
+				{ 0x3670, "PC", 2 },
+				{ 0x3674, "DWAYNE", 2 }
 			};
-			lineError = playSequence(responseLines, ARRAYSIZE(responseLines));
+			lineError = runtime.playDialogueEntrySequence(responseLines, ARRAYSIZE(responseLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			executeActionTagIfSet("PC_FRY_DADDY_1");
+			runtime.executeAndQueueAction("PC_FRY_DADDY_1");
 		}
 		return Common::kNoError;
 	}
@@ -599,36 +570,36 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		setDwayneIntroduced();
 
 		const DialogueLineEntry introLines[] = {
-			{ 0x3482, kDwayneNpc, 1 },
-			{ 0x3486, kPcSpeaker, 0 },
-			{ 0x348b, kDwayneNpc, 1 },
-			{ 0x3490, kEdnaNpc, 0 }
+			{ 0x3482, "DWAYNE", 1 },
+			{ 0x3486, "PC", 0 },
+			{ 0x348b, "DWAYNE", 1 },
+			{ 0x3490, "EDNA", 0 }
 		};
-		Common::Error lineError = playSequence(introLines, ARRAYSIZE(introLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(introLines, ARRAYSIZE(introLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (startupScript.getFlagValue("KARIN_KIDNAPED")) {
 			sharedState.karinKidnapedDialogueState = true;
-			lineError = playEdnaLine(0x3496, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x3496, "EDNA", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
-		lineError = playDwayneLine(0x349b, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x349b, "DWAYNE", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (state.pendingInitialConversation) {
 			const DialogueLineEntry initialLines[] = {
-				{ 0x34a0, kPcSpeaker, 0 },
-				{ 0x34a4, kDwayneNpc, 1 }
+				{ 0x34a0, "PC", 0 },
+				{ 0x34a4, "DWAYNE", 1 }
 			};
-			lineError = playSequence(initialLines, ARRAYSIZE(initialLines));
+			lineError = runtime.playDialogueEntrySequence(initialLines, ARRAYSIZE(initialLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 		handledIntroOrRevisit = true;
 	}
 	else if (state.sheriffInDinerIntroPlayed && sheriffInDiner) {
-		Common::Error lineError = playDwayneLine(0x34ac);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x34ac, "DWAYNE", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		handledIntroOrRevisit = true;
@@ -638,22 +609,22 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		setDwayneIntroduced();
 
 		const DialogueLineEntry initialLines[] = {
-			{ 0x3245, kDwayneNpc, 1 },
+			{ 0x3245, "DWAYNE", 1 },
 			{ 0x324a, "LOOMIS", 1 },
-			{ 0x324e, kPcSpeaker, 0 },
-			{ 0x3252, kDwayneNpc, 0 }
+			{ 0x324e, "PC", 0 },
+			{ 0x3252, "DWAYNE", 0 }
 		};
-		Common::Error lineError = playSequence(initialLines, ARRAYSIZE(initialLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(initialLines, ARRAYSIZE(initialLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		assignDwayneTopicBuffer(0x78);
+		topics.assign(0x78);
 		if (sharedState.boyleGascanApplicationState)
 			(void)startupScript.setRuntimeFlagValue("MOVE_SHERIFF", true);
 		handledIntroOrRevisit = true;
 	}
 
 	if (!handledIntroOrRevisit && state.eventFollowupGate) {
-		Common::Error lineError = playDwayneLine(0x34d3, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x34d3, "DWAYNE", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -661,7 +632,7 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (sharedState.dwayneWhaleyDisciplineFollowupState &&
 			!state.whaleyDisciplineFollowupShown) {
 		state.whaleyDisciplineFollowupShown = true;
-		Common::Error lineError = playDwayneLine(0x34b3);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x34b3, "DWAYNE", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -671,13 +642,13 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 		lineError = Common::kNoError;
 		if (responseIndex == 1) {
-			lineError = playDwayneLine(0x34bf, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x34bf, "DWAYNE", 2);
 		} else if (responseIndex == 2) {
-			lineError = playDwayneLine(0x34c5, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x34c5, "DWAYNE", 1);
 		}
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playDwayneLine(0x34cb, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x34cb, "DWAYNE", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -685,20 +656,20 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (sharedState.discussedNoteCheckbookEvidence != 0 &&
 			!state.noteCheckbookFollowupShown) {
 		state.noteCheckbookFollowupShown = true;
-		Common::Error lineError = playDwayneLine(0x355f, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x355f, "DWAYNE", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (sharedState.discussedTvDeedEvidence != 0) {
 			const DialogueLineEntry lines[] = {
-				{ 0x3565, kDwayneNpc, 2 },
-				{ 0x356a, kDwayneNpc, 2 },
-				{ 0x356f, kPcSpeaker, 2 },
-				{ 0x3573, kDwayneNpc, 0 }
+				{ 0x3565, "DWAYNE", 2 },
+				{ 0x356a, "DWAYNE", 2 },
+				{ 0x356f, "PC", 2 },
+				{ 0x3573, "DWAYNE", 0 }
 			};
-			lineError = playSequence(lines, ARRAYSIZE(lines));
+			lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			executeActionTagIfSet("PC_FRY_DADDY_1");
+			runtime.executeAndQueueAction("PC_FRY_DADDY_1");
 			return Common::kNoError;
 		}
 	}
@@ -707,14 +678,14 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (startupScript.getFlagValue("SCRATCHED_TUCKER") &&
 				!state.scratchedTuckerShown) {
 			state.scratchedTuckerShown = true;
-			Common::Error lineError = playDwayneLine(0x358b, 1);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x358b, "DWAYNE", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 		if (startupScript.getFlagValue("BOLT_OF_CLOTH_TAKEN") &&
 				!state.boltOfClothTakenShown) {
 			state.boltOfClothTakenShown = true;
-			Common::Error lineError = playDwayneLine(0x3594, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x3594, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -722,26 +693,26 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				!state.barberPoleStolenShown) {
 			state.barberPoleStolenShown = true;
 			const DialogueLineEntry lines[] = {
-				{ 0x359d, kDwayneNpc, 0 },
-				{ 0x35a4, kDwayneNpc, 0 },
-				{ 0x35ab, kDwayneNpc, 0 }
+				{ 0x359d, "DWAYNE", 0 },
+				{ 0x35a4, "DWAYNE", 0 },
+				{ 0x35ab, "DWAYNE", 0 }
 			};
-			Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 		if (startupScript.getFlagValue("DINER_BURNED") &&
 				!state.dinerBurnedShown) {
 			state.dinerBurnedShown = true;
-			Common::Error lineError = playDwayneLine(0x35bc, 3);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x35bc, "DWAYNE", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			if (startupScript.getFlagValue("KARIN_FOUND_ALIVE")) {
 				const DialogueLineEntry lines[] = {
-					{ 0x35c1, kDwayneNpc, 3 },
-					{ 0x35c5, kDwayneNpc, 3 }
+					{ 0x35c1, "DWAYNE", 3 },
+					{ 0x35c5, "DWAYNE", 3 }
 				};
-				lineError = playSequence(lines, ARRAYSIZE(lines));
+				lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
@@ -749,14 +720,14 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (startupScript.getFlagValue("PC_ESCAPED_JAIL") &&
 				!state.escapedJailShown) {
 			state.escapedJailShown = true;
-			Common::Error lineError = playDwayneLine(0x35cd);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x35cd, "DWAYNE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 		if (startupScript.getFlagValue("GOT_REMAINS_FOR_LODGE") &&
 				!state.gotRemainsForLodgeShown) {
 			state.gotRemainsForLodgeShown = true;
-			Common::Error lineError = playDwayneLine(0x35d7);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x35d7, "DWAYNE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -764,18 +735,18 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				!state.burnedTvStationShown) {
 			state.burnedTvStationShown = true;
 			const DialogueLineEntry lines[] = {
-				{ 0x35f6, kDwayneNpc, 2 },
-				{ 0x35fb, kDwayneNpc, 2 },
-				{ 0x3601, kDwayneNpc, 2 }
+				{ 0x35f6, "DWAYNE", 2 },
+				{ 0x35fb, "DWAYNE", 2 },
+				{ 0x3601, "DWAYNE", 2 }
 			};
-			Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 		if (startupScript.getFlagValue("KARIN_KIDNAPED") &&
 				!state.karinKidnapedShown) {
 			state.karinKidnapedShown = true;
-			Common::Error lineError = playDwayneLine(0x36b2, 3);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x36b2, "DWAYNE", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			sharedState.karinKidnapedDialogueState = true;
@@ -785,14 +756,14 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1) {
-				lineError = playDwayneLine(0x36bd);
+				lineError = runtime.playDialogueLineWithVariant(0x36bd, "DWAYNE", 0);
 			} else if (responseIndex == 2) {
-				lineError = playDwayneLine(0x36c1);
+				lineError = runtime.playDialogueLineWithVariant(0x36c1, "DWAYNE", 0);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			lineError = playDwayneLine(0x36c5);
+			lineError = runtime.playDialogueLineWithVariant(0x36c5, "DWAYNE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -801,14 +772,14 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1) {
-				lineError = playDwayneLine(0x36d0);
+				lineError = runtime.playDialogueLineWithVariant(0x36d0, "DWAYNE", 0);
 			} else if (responseIndex == 2) {
-				lineError = playDwayneLine(0x36d6, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x36d6, "DWAYNE", 1);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			lineError = playDwayneLine(0x36e7, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x36e7, "DWAYNE", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -822,11 +793,11 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		sharedState.dwaynePendingKarinAliveFollowupState = 0;
 
 		const DialogueLineEntry lines[] = {
-			{ 0x3780, kDwayneNpc, 0 },
-			{ 0x3786, kPcSpeaker, 4 },
-			{ 0x378a, kDwayneNpc, 2 }
+			{ 0x3780, "DWAYNE", 0 },
+			{ 0x3786, "PC", 4 },
+			{ 0x378a, "DWAYNE", 2 }
 		};
-		Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		state.completedKarinAliveFollowup = true;
@@ -839,7 +810,7 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!sheriffInDiner &&
 			!state.karinFoundDeadWithoutPurseShown) {
 		state.karinFoundDeadWithoutPurseShown = true;
-		Common::Error lineError = playDwayneLine(0x3793);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3793, "DWAYNE", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -848,9 +819,9 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (responseError.getCode() != Common::kNoError)
 			return responseError;
 		if (responseIndex == 1) {
-			return playDwayneLine(0x379d);
+			return runtime.playDialogueLineWithVariant(0x379d, "DWAYNE", 0);
 		} else if (responseIndex == 2) {
-			lineError = playDwayneLine(0x37a4, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x37a4, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -859,18 +830,18 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1) {
-				lineError = playDwayneLine(0x37ae);
+				lineError = runtime.playDialogueLineWithVariant(0x37ae, "DWAYNE", 0);
 			} else if (responseIndex == 2) {
-				lineError = playDwayneLine(0x37b4, 3);
+				lineError = runtime.playDialogueLineWithVariant(0x37b4, "DWAYNE", 3);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			const DialogueLineEntry lines[] = {
-				{ 0x37c1, kDwayneNpc, 0 },
-				{ 0x37c5, kPcSpeaker, 0 }
+				{ 0x37c1, "DWAYNE", 0 },
+				{ 0x37c5, "PC", 0 }
 			};
-			lineError = playSequence(lines, ARRAYSIZE(lines));
+			lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -880,11 +851,11 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!sheriffInDiner &&
 			!state.jimmyAbsentShown) {
 		state.jimmyAbsentShown = true;
-		Common::Error lineError = playDwayneLine(0x3855, 3);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3855, "DWAYNE", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (startupScript.getFlagValue("JIMMY_ATTACKED")) {
-			lineError = playDwayneLine(0x37f8);
+			lineError = runtime.playDialogueLineWithVariant(0x37f8, "DWAYNE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -894,9 +865,9 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 			lineError = Common::kNoError;
 			if (responseIndex == 1) {
-				lineError = playDwayneLine(0x3805, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x3805, "DWAYNE", 1);
 			} else if (responseIndex == 2) {
-				lineError = playDwayneLine(0x380c);
+				lineError = runtime.playDialogueLineWithVariant(0x380c, "DWAYNE", 0);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -907,7 +878,7 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!sheriffInDiner &&
 			!state.moynahanAbsentShown) {
 		state.moynahanAbsentShown = true;
-		Common::Error lineError = playDwayneLine(0x38ce);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x38ce, "DWAYNE", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -915,7 +886,7 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (startupScript.getFlagValue("EDNA_HUNG") &&
 			!state.ednaHungShown) {
 		state.ednaHungShown = true;
-		Common::Error lineError = playDwayneLine(0x38f1, 3);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x38f1, "DWAYNE", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -925,8 +896,8 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.mcknightAbsentShown) {
 		state.mcknightAbsentShown = true;
 		Common::Error lineError = startupScript.isObjectInInventory("TV_DEED")
-			? playDwayneLine(0x3904, 3)
-			: playDwayneLine(0x390f, 1);
+			? runtime.playDialogueLineWithVariant(0x3904, "DWAYNE", 3)
+			: runtime.playDialogueLineWithVariant(0x390f, "DWAYNE", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -934,7 +905,7 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	Common::String selectedTopic;
 	for (;;) {
 		if (runtime.matchesResponseLine(selectedTopic, 0x82))
-			return playDwayneLine(0x3395);
+			return runtime.playDialogueLineWithVariant(0x3395, "DWAYNE", 0);
 
 		Common::Error menuError = runtime.runKeywordMenu(
 			dwayneTopicBuffer, dwayneTopicBufferLineIndex, selectedTopic);
@@ -944,10 +915,10 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return Common::kNoError;
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x83)) {
-			Common::Error lineError = playDwayneLine(0x326e);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x326e, "DWAYNE", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignDwayneTopicBuffer(0x84);
+			topics.assign(0x84);
 			continue;
 		}
 
@@ -955,20 +926,20 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				ARRAYSIZE(kDwayneWhaleyTopicResponseLines))) {
 			if (!sheriffInDiner) {
 				const DialogueLineEntry lines[] = {
-					{ 0x327b, kDwayneNpc, 0 },
-					{ 0x3280, kPcSpeaker, 2 },
-					{ 0x3285, kDwayneNpc, 0 },
+					{ 0x327b, "DWAYNE", 0 },
+					{ 0x3280, "PC", 2 },
+					{ 0x3285, "DWAYNE", 0 },
 					{ 0x328b, "LOOMIS", 1 },
-					{ 0x3290, kDwayneNpc, 2 },
+					{ 0x3290, "DWAYNE", 2 },
 					{ 0x3294, "LOOMIS", 3 },
-					{ 0x3299, kPcSpeaker, 4 },
-					{ 0x329e, kDwayneNpc, 1 }
+					{ 0x3299, "PC", 4 },
+					{ 0x329e, "DWAYNE", 1 }
 				};
-				Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+				Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				sharedState.dialogueStateD2f04 = true;
-				assignDwayneTopicBuffer(0x89);
+				topics.assign(0x89);
 			}
 			continue;
 		}
@@ -977,46 +948,46 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				ARRAYSIZE(kDwayneLoomisTopicResponseLines))) {
 			if (!sheriffInDiner) {
 				const DialogueLineEntry lines[] = {
-					{ 0x32bf, kDwayneNpc, 1 },
-					{ 0x32c1, kDwayneNpc, 0 },
+					{ 0x32bf, "DWAYNE", 1 },
+					{ 0x32c1, "DWAYNE", 0 },
 					{ 0x32c7, "LOOMIS", 3 },
-					{ 0x32cb, kDwayneNpc, 0 },
+					{ 0x32cb, "DWAYNE", 0 },
 					{ 0x32d1, "LOOMIS", 1 }
 				};
-				Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+				Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				runtime.setActiveSpeakerPortrait(kDwayneNpc, 0);
-				assignDwayneTopicBuffer(0x8e);
+				runtime.setActiveSpeakerPortrait("DWAYNE", 0);
+				topics.assign(0x8e);
 			}
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x8f)) {
 			const DialogueLineEntry lines[] = {
-				{ 0x32db, kDwayneNpc, 0 },
-				{ 0x32e2, kPcSpeaker, 4 },
-				{ 0x32e6, kDwayneNpc, 0 }
+				{ 0x32db, "DWAYNE", 0 },
+				{ 0x32e2, "PC", 4 },
+				{ 0x32e6, "DWAYNE", 0 }
 			};
-			Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignDwayneTopicBuffer(0x90);
+			topics.assign(0x90);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x91)) {
 			const DialogueLineEntry lines[] = {
-				{ 0x32f2, kDwayneNpc, 0 },
-				{ 0x32f9, kPcSpeaker, 0 },
-				{ 0x32fd, kDwayneNpc, 0 },
-				{ 0x32fe, kDwayneNpc, 0 },
-				{ 0x32ff, kDwayneNpc, 0 }
+				{ 0x32f2, "DWAYNE", 0 },
+				{ 0x32f9, "PC", 0 },
+				{ 0x32fd, "DWAYNE", 0 },
+				{ 0x32fe, "DWAYNE", 0 },
+				{ 0x32ff, "DWAYNE", 0 }
 			};
-			Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignDwayneTopicBuffer(0x92);
+			topics.assign(0x92);
 			continue;
 		}
 
@@ -1024,30 +995,30 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			Common::Error lineError = Common::kNoError;
 			if (!startupScript.getFlagValue("STEPH_MIDGAME_PLAYED")) {
 				const DialogueLineEntry lines[] = {
-					{ 0x330d, kDwayneNpc, 1 },
-					{ 0x3314, kPcSpeaker, 0 },
-					{ 0x3318, kDwayneNpc, 0 },
-					{ 0x332c, kDwayneNpc, 0 }
+					{ 0x330d, "DWAYNE", 1 },
+					{ 0x3314, "PC", 0 },
+					{ 0x3318, "DWAYNE", 0 },
+					{ 0x332c, "DWAYNE", 0 }
 				};
-				lineError = playSequence(lines, ARRAYSIZE(lines));
+				lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			} else {
 				const DialogueLineEntry lines[] = {
-					{ 0x331f, kDwayneNpc, 3 },
-					{ 0x3320, kDwayneNpc, 0 },
-					{ 0x3321, kDwayneNpc, 0 },
-					{ 0x332c, kDwayneNpc, 0 }
+					{ 0x331f, "DWAYNE", 3 },
+					{ 0x3320, "DWAYNE", 0 },
+					{ 0x3321, "DWAYNE", 0 },
+					{ 0x332c, "DWAYNE", 0 }
 				};
-				lineError = playSequence(lines, ARRAYSIZE(lines));
+				lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignDwayneTopicBuffer(0x94);
+			topics.assign(0x94);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x95)) {
 			sharedState.discussedLodgeTopic = true;
-			Common::Error lineError = playDwayneLine(0x3339, 1);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x3339, "DWAYNE", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -1058,65 +1029,65 @@ Common::Error DwayneDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			lineError = Common::kNoError;
 			if (responseIndex == 1) {
 				const DialogueLineEntry responseLines[] = {
-					{ 0x3345, kDwayneNpc, 2 },
-					{ 0x3346, kDwayneNpc, 2 },
-					{ 0x3347, kDwayneNpc, 2 }
+					{ 0x3345, "DWAYNE", 2 },
+					{ 0x3346, "DWAYNE", 2 },
+					{ 0x3347, "DWAYNE", 2 }
 				};
-				lineError = playSequence(responseLines, ARRAYSIZE(responseLines));
+				lineError = runtime.playDialogueEntrySequence(responseLines, ARRAYSIZE(responseLines));
 			} else if (responseIndex == 2) {
-				lineError = playDwayneLine(0x3350);
+				lineError = runtime.playDialogueLineWithVariant(0x3350, "DWAYNE", 0);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignDwayneTopicBuffer(0x97);
+			topics.assign(0x97);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x98)) {
 			const DialogueLineEntry lines[] = {
-				{ 0x335b, kDwayneNpc, 2 },
-				{ 0x3360, kPcSpeaker, 2 },
-				{ 0x3364, kDwayneNpc, 3 }
+				{ 0x335b, "DWAYNE", 2 },
+				{ 0x3360, "PC", 2 },
+				{ 0x3364, "DWAYNE", 3 }
 			};
-			Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = sharedState.dialogueStateD2f04
-				? runtime.playDialogueLineWithVariant(0x3368, kPcSpeaker, 2)
-				: runtime.playDialogueLineWithVariant(0x336c, kPcSpeaker, 2);
+				? runtime.playDialogueLineWithVariant(0x3368, "PC", 2)
+				: runtime.playDialogueLineWithVariant(0x336c, "PC", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playDwayneLine(0x3370, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x3370, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignDwayneTopicBuffer(0x99);
+			topics.assign(0x99);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x9a)) {
-			Common::Error lineError = playDwayneLine(0x337a, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x337a, "DWAYNE", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignDwayneTopicBuffer(0x9b);
+			topics.assign(0x9b);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x9c)) {
 			const DialogueLineEntry lines[] = {
-				{ 0x3386, kDwayneNpc, 0 },
-				{ 0x3387, kDwayneNpc, 0 }
+				{ 0x3386, "DWAYNE", 0 },
+				{ 0x3387, "DWAYNE", 0 }
 			};
-			Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignDwayneTopicBuffer(0x9d);
+			topics.assign(0x9d);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x9e))
 			continue;
 
-		Common::Error lineError = playDwayneLine(0x393c);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x393c, "DWAYNE", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
diff --git a/engines/harvester/npc/dwayne_dialogue.h b/engines/harvester/npc/dwayne_dialogue.h
index a5106960d8e..fad96e2169a 100644
--- a/engines/harvester/npc/dwayne_dialogue.h
+++ b/engines/harvester/npc/dwayne_dialogue.h
@@ -61,7 +61,6 @@ public:
 		int currentTopicBufferLineIndex = -1;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = DwayneRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.pendingInitialConversation);
diff --git a/engines/harvester/npc/edna_dialogue.cpp b/engines/harvester/npc/edna_dialogue.cpp
index 8c569f376d4..6ee0d30fa0a 100644
--- a/engines/harvester/npc/edna_dialogue.cpp
+++ b/engines/harvester/npc/edna_dialogue.cpp
@@ -29,8 +29,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kEdnaNpc = "EDNA";
-static const char *const kPcSpeaker = "PC";
 static const char *const kRahRoomName = "RAH";
 static const char *const kDialogueC043FstPath = "GRAPHIC/FST/C043.FST";
 static const char *const kDialogueC043KFstPath = "GRAPHIC/FST/C043K.FST";
@@ -49,65 +47,61 @@ static const int kEdnaLodgeTopicLineIndices[] = { 0xb6, 0xb7 };
 static const int kEdnaBcBdBeTopicLineIndices[] = { 0xbc, 0xbd, 0xbe };
 
 static const DialogueLineEntry kEdnaBringKarinToSheriffLines[] = {
-	{ 0x3cce, kEdnaNpc, 2 },
-	{ 0x3cd2, kPcSpeaker, 0 },
-	{ 0x3cd7, kEdnaNpc, 1 }
+	{ 0x3cce, "EDNA", 2 },
+	{ 0x3cd2, "PC", 0 },
+	{ 0x3cd7, "EDNA", 1 }
 };
 
 static const DialogueLineEntry kEdnaIntroResponseTwoLines[] = {
-	{ 0x3ab2, kEdnaNpc, 0 },
-	{ 0x3ab7, kPcSpeaker, 0 },
-	{ 0x3abb, kEdnaNpc, 0 },
-	{ 0x3ac2, kPcSpeaker, 0 }
+	{ 0x3ab2, "EDNA", 0 },
+	{ 0x3ab7, "PC", 0 },
+	{ 0x3abb, "EDNA", 0 },
+	{ 0x3ac2, "PC", 0 }
 };
 
 static const DialogueLineEntry kEdnaDayFiveAliveResponseOneLines[] = {
-	{ 0x3c23, kEdnaNpc, 0 },
-	{ 0x3c29, kPcSpeaker, 0 },
-	{ 0x3c2e, kEdnaNpc, 3 }
+	{ 0x3c23, "EDNA", 0 },
+	{ 0x3c29, "PC", 0 },
+	{ 0x3c2e, "EDNA", 3 }
 };
 
 static const DialogueLineEntry kEdnaBarberPoleStolenLines[] = {
-	{ 0x3c4e, kEdnaNpc, 3 },
-	{ 0x3c54, kPcSpeaker, 0 },
-	{ 0x3c5a, kEdnaNpc, 2 }
+	{ 0x3c4e, "EDNA", 3 },
+	{ 0x3c54, "PC", 0 },
+	{ 0x3c5a, "EDNA", 2 }
 };
 
 static const DialogueLineEntry kEdnaKarinFoundDeadIntroLines[] = {
-	{ 0x3cf0, kEdnaNpc, 2 },
-	{ 0x3cf5, kPcSpeaker, 0 },
-	{ 0x3cf9, kEdnaNpc, 2 }
+	{ 0x3cf0, "EDNA", 2 },
+	{ 0x3cf5, "PC", 0 },
+	{ 0x3cf9, "EDNA", 2 }
 };
 
 static const DialogueLineEntry kEdnaTopicB2Lines[] = {
-	{ 0x3b15, kEdnaNpc, 1 },
-	{ 0x3b1a, kPcSpeaker, 0 },
-	{ 0x3b1e, kEdnaNpc, 0 }
+	{ 0x3b15, "EDNA", 1 },
+	{ 0x3b1a, "PC", 0 },
+	{ 0x3b1e, "EDNA", 0 }
 };
 
 static const DialogueLineEntry kEdnaLodgeTopicLines[] = {
-	{ 0x3b57, kEdnaNpc, 0 },
-	{ 0x3b5d, kPcSpeaker, 0 },
-	{ 0x3b61, kEdnaNpc, 0 },
-	{ 0x3b62, kEdnaNpc, 0 },
-	{ 0x3b63, kEdnaNpc, 0 },
-	{ 0x3b6c, kPcSpeaker, 0 },
-	{ 0x3b70, kEdnaNpc, 0 },
-	{ 0x3b75, kPcSpeaker, 0 },
-	{ 0x3b79, kEdnaNpc, 0 }
+	{ 0x3b57, "EDNA", 0 },
+	{ 0x3b5d, "PC", 0 },
+	{ 0x3b61, "EDNA", 0 },
+	{ 0x3b62, "EDNA", 0 },
+	{ 0x3b63, "EDNA", 0 },
+	{ 0x3b6c, "PC", 0 },
+	{ 0x3b70, "EDNA", 0 },
+	{ 0x3b75, "PC", 0 },
+	{ 0x3b79, "EDNA", 0 }
 };
 
 static const DialogueLineEntry kEdnaTopicBcBdBeLines[] = {
-	{ 0x3b8e, kEdnaNpc, 0 },
-	{ 0x3b8f, kEdnaNpc, 0 }
+	{ 0x3b8e, "EDNA", 0 },
+	{ 0x3b8f, "EDNA", 0 }
 };
 
 } // End of namespace
 
-bool EdnaDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kEdnaNpc);
-}
-
 Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	EdnaRoomDialogueState &state = _state;
@@ -115,19 +109,11 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	Common::String topicBuffer;
 	int topicBufferLineIndex = -1;
 
-	auto assignEdnaTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(topicBuffer, topicBufferLineIndex,
-			responseLineIndex, "Edna topic buffer");
-	};
-	auto playEdnaLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kEdnaNpc, headVariant);
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
+	DialogueTopicBuffer topics(runtime, topicBuffer, topicBufferLineIndex, "Edna topic buffer");
+
 	auto completeKarinPurseExchange = [&]() -> Common::Error {
 		state.karinPurseLinePlayed = true;
-		Common::Error lineError = playEdnaLine(0x3d05, 3);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3d05, "EDNA", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -137,7 +123,7 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	};
 
 	if (startupScript.getFlagValue("KILLED_KARIN1")) {
-		Common::Error lineError = playEdnaLine(0x3cdf, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3cdf, "EDNA", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		(void)startupScript.setRuntimeFlagValue("KILLED_KARIN1", false);
@@ -166,19 +152,19 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				usedItemName.equalsIgnoreCase("CASKET_PHOTOCOPY")) {
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownPhotoOfCorpse, true);
 			sharedState.discussedCasketPhotoEvidence = 1;
-			return playEdnaLine(0x3bd6);
+			return runtime.playDialogueLineWithVariant(0x3bd6, "EDNA", 0);
 		}
 		if ((usedItemName.equalsIgnoreCase("LEDGER") ||
 				usedItemName.equalsIgnoreCase("LEDGER2")) &&
 				startupScript.getFlagValue("HAVE_BOTH_LEDGERS")) {
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownLedgersToAnyone, true);
 			sharedState.discussedLedgerEvidence = 1;
-			return playEdnaLine(0x3bd6);
+			return runtime.playDialogueLineWithVariant(0x3bd6, "EDNA", 0);
 		}
 		if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownPhotoOfWhaleyHerrill, true);
 			sharedState.discussedWhaleyHerrillPhoto = 1;
-			return playEdnaLine(0x3bde, 2);
+			return runtime.playDialogueLineWithVariant(0x3bde, "EDNA", 2);
 		}
 		if (usedItemName.equalsIgnoreCase("NOTE") ||
 				usedItemName.equalsIgnoreCase("NOTE_PHOTOCOPY") ||
@@ -186,21 +172,21 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				usedItemName.equalsIgnoreCase("CHECKBOOK_PHOTOCOPY")) {
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownEvidenceOfBlackmail, true);
 			sharedState.discussedNoteCheckbookEvidence = 1;
-			return playEdnaLine(0x3be5, 2);
+			return runtime.playDialogueLineWithVariant(0x3be5, "EDNA", 2);
 		}
 		if (usedItemName.equalsIgnoreCase("TV_DEED") ||
 				usedItemName.equalsIgnoreCase("TV_DEED_PHOTOCOPY")) {
 			(void)startupScript.setRuntimeFlagValue(DialogueFlags::kShownEvidenceSheriffOwns, true);
 			sharedState.discussedTvDeedEvidence = 1;
-			return playEdnaLine(0x3bee);
+			return runtime.playDialogueLineWithVariant(0x3bee, "EDNA", 0);
 		}
 
-		return playEdnaLine(0x3bcf);
+		return runtime.playDialogueLineWithVariant(0x3bcf, "EDNA", 0);
 	}
 
 	if (startupScript.getFlagValue("BRING_KARIN_TO_SHERIFF")) {
-		Common::Error lineError = playSequence(
-			kEdnaBringKarinToSheriffLines, ARRAYSIZE(kEdnaBringKarinToSheriffLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kEdnaBringKarinToSheriffLines,
+			ARRAYSIZE(kEdnaBringKarinToSheriffLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -211,7 +197,7 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	if (state.introPending) {
 		state.introPending = false;
-		Common::Error lineError = playEdnaLine(0x3a95);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3a95, "EDNA", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -221,44 +207,44 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playEdnaLine(0x3aa1, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x3aa1, "EDNA", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueLine(0x3aa6, "PC");
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playEdnaLine(0x3aaa);
+			lineError = runtime.playDialogueLineWithVariant(0x3aaa, "EDNA", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			sharedState.dialogueStateD2f04 = true;
-			lineError = runtime.playDialogueLine(0x3aae, kPcSpeaker);
+			lineError = runtime.playDialogueLine(0x3aae, "PC");
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else if (responseIndex == 2) {
-			lineError = playSequence(kEdnaIntroResponseTwoLines, ARRAYSIZE(kEdnaIntroResponseTwoLines));
+			lineError = runtime.playDialogueEntrySequence(kEdnaIntroResponseTwoLines, ARRAYSIZE(kEdnaIntroResponseTwoLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 
-		lineError = playEdnaLine(0x3ac6, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x3ac6, "EDNA", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playEdnaLine(0x3aca);
+		lineError = runtime.playDialogueLineWithVariant(0x3aca, "EDNA", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playEdnaLine(0x3acf);
+		lineError = runtime.playDialogueLineWithVariant(0x3acf, "EDNA", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		assignEdnaTopicBuffer(kEdnaInitialTopicBufferLine);
+		topics.assign(kEdnaInitialTopicBufferLine);
 	}
 
 	if (!startupScript.getFlagValue("KARIN_KIDNAPED")) {
-		Common::Error lineError = playEdnaLine(0x3bf8);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3bf8, "EDNA", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	} else {
-		Common::Error lineError = playEdnaLine(0x3bff);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3bff, "EDNA", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		sharedState.karinKidnapedDialogueState = true;
@@ -267,7 +253,7 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (startupScript.getFlagValue("DAY_5") && !state.dayFiveShown) {
 		state.dayFiveShown = true;
 		if (startupScript.getFlagValue("KARIN_FOUND_ALIVE")) {
-			Common::Error lineError = playEdnaLine(0x3c17);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x3c17, "EDNA", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -277,17 +263,17 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1) {
-				lineError = playSequence(kEdnaDayFiveAliveResponseOneLines,
+				lineError = runtime.playDialogueEntrySequence(kEdnaDayFiveAliveResponseOneLines,
 					ARRAYSIZE(kEdnaDayFiveAliveResponseOneLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else if (responseIndex == 2) {
-				lineError = playEdnaLine(0x3c33);
+				lineError = runtime.playDialogueLineWithVariant(0x3c33, "EDNA", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 		} else if (startupScript.getFlagValue("KARIN_FOUND_DEAD")) {
-			Common::Error lineError = playEdnaLine(0x3c3a, 3);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x3c3a, "EDNA", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -295,43 +281,43 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	if (startupScript.getFlagValue("SCRATCHED_TUCKER") && !state.scratchedTuckerShown) {
 		state.scratchedTuckerShown = true;
-		Common::Error lineError = playEdnaLine(0x3c45);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3c45, "EDNA", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (startupScript.getFlagValue("BARBER_POLE_STOLEN") &&
 			!state.barberPoleStolenShown) {
 		state.barberPoleStolenShown = true;
-		Common::Error lineError = playSequence(
-			kEdnaBarberPoleStolenLines, ARRAYSIZE(kEdnaBarberPoleStolenLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kEdnaBarberPoleStolenLines,
+			ARRAYSIZE(kEdnaBarberPoleStolenLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (startupScript.getFlagValue("BOLT_OF_CLOTH_TAKEN") &&
 			!state.boltOfClothTakenShown) {
 		state.boltOfClothTakenShown = true;
-		Common::Error lineError = playEdnaLine(0x3c62);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3c62, "EDNA", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (startupScript.getFlagValue("PC_ESCAPED_JAIL") &&
 			!state.escapedJailShown) {
 		state.escapedJailShown = true;
-		Common::Error lineError = playEdnaLine(0x3c92, 3);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3c92, "EDNA", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (startupScript.getFlagValue("BURNED_TV_STATION") &&
 			!state.burnedTvStationShown) {
 		state.burnedTvStationShown = true;
-		Common::Error lineError = playEdnaLine(0x3ca2, 3);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3ca2, "EDNA", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (startupScript.getFlagValue("KARIN_FOUND_DEAD") &&
 			!state.karinFoundDeadShown) {
 		state.karinFoundDeadShown = true;
-		Common::Error lineError = playEdnaLine(0x3ce5, 3);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3ce5, "EDNA", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -341,8 +327,8 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (responseError.getCode() != Common::kNoError)
 			return responseError;
 		if (responseIndex == 1) {
-			lineError = playSequence(
-				kEdnaKarinFoundDeadIntroLines, ARRAYSIZE(kEdnaKarinFoundDeadIntroLines));
+			lineError = runtime.playDialogueEntrySequence(kEdnaKarinFoundDeadIntroLines,
+				ARRAYSIZE(kEdnaKarinFoundDeadIntroLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -364,12 +350,12 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 						return lineError;
 				}
 			} else if (responseIndex == 2) {
-				lineError = playEdnaLine(0x3d0a, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x3d0a, "EDNA", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 		} else if (responseIndex == 2) {
-			lineError = playEdnaLine(0x3d0e, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x3d0e, "EDNA", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -389,7 +375,7 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (!startupScript.getFlagValue("KARIN_KIDNAPED") &&
 					!startupScript.getFlagValue("KARIN_FOUND_DEAD") &&
 					!startupScript.getFlagValue("KARIN_FOUND_ALIVE")) {
-				Common::Error lineError = playEdnaLine(0x3bc3, 1);
+				Common::Error lineError = runtime.playDialogueLineWithVariant(0x3bc3, "EDNA", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
@@ -399,22 +385,21 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesResponseLine(selectedTopic, 0xa6)) {
 			if (!startupScript.getFlagValue("KARIN_KIDNAPED") &&
 					!startupScript.getFlagValue("KARIN_FOUND_DEAD")) {
-				Common::Error lineError = playEdnaLine(0x3ad9, 1);
+				Common::Error lineError = runtime.playDialogueLineWithVariant(0x3ad9, "EDNA", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				lineError = runtime.playDialogueFst(kDialogueC091FstPath);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else {
-				Common::Error lineError = playEdnaLine(0x3ae3, 3);
+				Common::Error lineError = runtime.playDialogueLineWithVariant(0x3ae3, "EDNA", 3);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				if (!startupScript.getFlagValue("KARIN_FOUND_DEAD")) {
-					lineError = playEdnaLine(0x3ae9, 3);
+					lineError = runtime.playDialogueLineWithVariant(0x3ae9, "EDNA", 3);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
-					assignEdnaTopicBuffer(
-						startupScript.getFlagValue("SHERIFF_IN_DINER") ? 0xab : 0xaa);
+					topics.assign(startupScript.getFlagValue("SHERIFF_IN_DINER") ? 0xab : 0xaa);
 				}
 			}
 			continue;
@@ -422,31 +407,31 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (runtime.matchesAnyResponseLine(selectedTopic, kEdnaAcAdTopicLineIndices,
 					ARRAYSIZE(kEdnaAcAdTopicLineIndices))) {
-			Common::Error lineError = playEdnaLine(0x3af6);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x3af6, "EDNA", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playEdnaLine(0x3af8);
+			lineError = runtime.playDialogueLineWithVariant(0x3af8, "EDNA", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignEdnaTopicBuffer(startupScript.getFlagValue("SHERIFF_IN_DINER") ? 0xaf : 0xae);
+			topics.assign(startupScript.getFlagValue("SHERIFF_IN_DINER") ? 0xaf : 0xae);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0xb0)) {
 			if (!startupScript.getFlagValue("SHERIFF_IN_DINER")) {
-				Common::Error lineError = playEdnaLine(0x3b03);
+				Common::Error lineError = runtime.playDialogueLineWithVariant(0x3b03, "EDNA", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playEdnaLine(0x3b07);
+				lineError = runtime.playDialogueLineWithVariant(0x3b07, "EDNA", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignEdnaTopicBuffer(0xb1);
+				topics.assign(0xb1);
 			}
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0xb2)) {
-			Common::Error lineError = playSequence(kEdnaTopicB2Lines, ARRAYSIZE(kEdnaTopicB2Lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kEdnaTopicB2Lines, ARRAYSIZE(kEdnaTopicB2Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
@@ -455,19 +440,19 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesAnyResponseLine(selectedTopic, kEdnaLodgeTopicLineIndices,
 					ARRAYSIZE(kEdnaLodgeTopicLineIndices))) {
 			sharedState.discussedLodgeTopic = true;
-			Common::Error lineError = playSequence(kEdnaLodgeTopicLines, ARRAYSIZE(kEdnaLodgeTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kEdnaLodgeTopicLines, ARRAYSIZE(kEdnaLodgeTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignEdnaTopicBuffer(startupScript.getFlagValue("SHERIFF_IN_DINER") ? 0xb9 : 0xb8);
+			topics.assign(startupScript.getFlagValue("SHERIFF_IN_DINER") ? 0xb9 : 0xb8);
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0xba)) {
 			if (!startupScript.getFlagValue("SHERIFF_IN_DINER")) {
-				Common::Error lineError = playEdnaLine(0x3b82);
+				Common::Error lineError = runtime.playDialogueLineWithVariant(0x3b82, "EDNA", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignEdnaTopicBuffer(0xbb);
+				topics.assign(0xbb);
 			}
 			continue;
 		}
@@ -475,18 +460,18 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesAnyResponseLine(selectedTopic, kEdnaBcBdBeTopicLineIndices,
 					ARRAYSIZE(kEdnaBcBdBeTopicLineIndices))) {
 			if (!startupScript.getFlagValue("SHERIFF_IN_DINER")) {
-				Common::Error lineError = playSequence(
-					kEdnaTopicBcBdBeLines, ARRAYSIZE(kEdnaTopicBcBdBeLines));
+				Common::Error lineError = runtime.playDialogueEntrySequence(kEdnaTopicBcBdBeLines,
+					ARRAYSIZE(kEdnaTopicBcBdBeLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignEdnaTopicBuffer(0xbf);
+				topics.assign(0xbf);
 			}
 			continue;
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, 0xc0)) {
 			if (startupScript.getFlagValue("KARIN_KIDNAPED")) {
-				Common::Error lineError = playEdnaLine(0x3b9e, 2);
+				Common::Error lineError = runtime.playDialogueLineWithVariant(0x3b9e, "EDNA", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				sharedState.karinKidnapedDialogueState = true;
@@ -496,7 +481,7 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				if (responseError.getCode() != Common::kNoError)
 					return responseError;
 
-				lineError = playEdnaLine(responseIndex == 1 ? 0x3bad : 0x3bb3, 3);
+				lineError = runtime.playDialogueLineWithVariant(responseIndex == 1 ? 0x3bad : 0x3bb3, "EDNA", 3);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				lineError = runtime.playDialogueFst(kDialogueC070FstPath);
@@ -510,7 +495,7 @@ Common::Error EdnaDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesResponseLine(selectedTopic, 0xc2))
 			continue;
 
-		Common::Error lineError = playEdnaLine(0x3bc9);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3bc9, "EDNA", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
diff --git a/engines/harvester/npc/edna_dialogue.h b/engines/harvester/npc/edna_dialogue.h
index 5db136900d3..a83f447de60 100644
--- a/engines/harvester/npc/edna_dialogue.h
+++ b/engines/harvester/npc/edna_dialogue.h
@@ -40,7 +40,6 @@ public:
 		bool karinFoundDeadShown = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = EdnaRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.introPending);
diff --git a/engines/harvester/npc/fireman1_dialogue.cpp b/engines/harvester/npc/fireman1_dialogue.cpp
index f6812ff0ee8..46bacf2effa 100644
--- a/engines/harvester/npc/fireman1_dialogue.cpp
+++ b/engines/harvester/npc/fireman1_dialogue.cpp
@@ -26,16 +26,6 @@
 
 namespace Harvester {
 
-namespace {
-
-static const char *const kFireman1Npc = "FIREMAN1";
-
-} // End of namespace
-
-bool Fireman1DialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kFireman1Npc);
-}
-
 Common::Error Fireman1DialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
 	Fireman1RoomDialogueState &state = _state;
@@ -43,9 +33,9 @@ Common::Error Fireman1DialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	Common::Error lineError = Common::kNoError;
 	if (state.talkStatePending) {
 		state.talkStatePending = false;
-		lineError = runtime.playDialogueLine(0x453, kFireman1Npc);
+		lineError = runtime.playDialogueLine(0x453, "FIREMAN1");
 	} else {
-		lineError = runtime.playDialogueLine(0x463, kFireman1Npc);
+		lineError = runtime.playDialogueLine(0x463, "FIREMAN1");
 	}
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
@@ -53,7 +43,7 @@ Common::Error Fireman1DialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED") &&
 			!state.stephMidgamePlayedShown) {
 		state.stephMidgamePlayedShown = true;
-		lineError = runtime.playDialogueLineWithVariant(0x45a, kFireman1Npc, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x45a, "FIREMAN1", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -61,7 +51,7 @@ Common::Error Fireman1DialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("BOLT_OF_CLOTH_TAKEN") &&
 			!state.boltOfClothTakenShown) {
 		state.boltOfClothTakenShown = true;
-		lineError = runtime.playDialogueLineWithVariant(0x46a, kFireman1Npc, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x46a, "FIREMAN1", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
diff --git a/engines/harvester/npc/fireman1_dialogue.h b/engines/harvester/npc/fireman1_dialogue.h
index e907af54a4a..2fb1648d4b4 100644
--- a/engines/harvester/npc/fireman1_dialogue.h
+++ b/engines/harvester/npc/fireman1_dialogue.h
@@ -34,7 +34,6 @@ public:
 		bool boltOfClothTakenShown = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = Fireman1RoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.talkStatePending);
diff --git a/engines/harvester/npc/fireman2_dialogue.cpp b/engines/harvester/npc/fireman2_dialogue.cpp
index 302d18a2864..7616a26228c 100644
--- a/engines/harvester/npc/fireman2_dialogue.cpp
+++ b/engines/harvester/npc/fireman2_dialogue.cpp
@@ -26,10 +26,6 @@
 
 namespace Harvester {
 
-bool Fireman2DialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase("FIREMAN2");
-}
-
 Common::Error Fireman2DialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
 	int wavId = 0x5c7;
diff --git a/engines/harvester/npc/fireman2_dialogue.h b/engines/harvester/npc/fireman2_dialogue.h
index 329ddddb895..d41fb329449 100644
--- a/engines/harvester/npc/fireman2_dialogue.h
+++ b/engines/harvester/npc/fireman2_dialogue.h
@@ -34,7 +34,6 @@ public:
 		int dialogueStateD2de4 = 0;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = Fireman2RoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.talkStateBlock);
diff --git a/engines/harvester/npc/gladiator_dialogue.cpp b/engines/harvester/npc/gladiator_dialogue.cpp
index 2a1b5926c4b..2a36fd292eb 100644
--- a/engines/harvester/npc/gladiator_dialogue.cpp
+++ b/engines/harvester/npc/gladiator_dialogue.cpp
@@ -28,7 +28,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kGladiatorNpc = "GLADIATOR";
 static const char *const kGladiatorInterruptFlag = "GLADIATOR_INTERRUPT_CONVERSATION";
 static const char *const kStartMercyTimerActionTag = "START_MERCY_TIMR";
 static const char *const kDialogueC110FstPath = "GRAPHIC/FST/C110.FST";
@@ -37,29 +36,22 @@ static const int kGladiatorFollowupResponseLine = 0xc4;
 
 } // End of namespace
 
-bool GladiatorDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kGladiatorNpc);
-}
-
 Common::Error GladiatorDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
-	auto playGladiatorLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kGladiatorNpc, headVariant);
-	};
 	auto queueGladiatorMonsterfyTransition = [&]() {
 		InteractionResult interaction;
-		if (runtime.startupScript().queueRuntimeNpcDeathOrMonsterfy(kGladiatorNpc)) {
+		if (runtime.startupScript().queueRuntimeNpcDeathOrMonsterfy("GLADIATOR")) {
 			interaction.mutatedRuntimeState = true;
 			interaction.visualRuntimeStateChanged = true;
 		}
 		runtime.queueDialogueInteractionIfNeeded(interaction);
 	};
 	auto runGladiatorDeathBranch = [&](int leadWavId) -> Common::Error {
-		Common::Error lineError = playGladiatorLine(leadWavId, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(leadWavId, "GLADIATOR", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playGladiatorLine(0x6cd, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x6cd, "GLADIATOR", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -72,11 +64,11 @@ Common::Error GladiatorDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	(void)runtime.startupScript().setRuntimeFlagValue(kGladiatorInterruptFlag, false);
 
-	Common::Error lineError = playGladiatorLine(0x69c, 1);
+	Common::Error lineError = runtime.playDialogueLineWithVariant(0x69c, "GLADIATOR", 1);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playGladiatorLine(0x6a3, 0);
+	lineError = runtime.playDialogueLineWithVariant(0x6a3, "GLADIATOR", 0);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -91,7 +83,7 @@ Common::Error GladiatorDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (responseIndex != 2)
 		return Common::kNoError;
 
-	lineError = playGladiatorLine(0x6b3, 0);
+	lineError = runtime.playDialogueLineWithVariant(0x6b3, "GLADIATOR", 0);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -110,11 +102,11 @@ Common::Error GladiatorDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (responseIndex != 2)
 		return Common::kNoError;
 
-	lineError = playGladiatorLine(0x6c7, 1);
+	lineError = runtime.playDialogueLineWithVariant(0x6c7, "GLADIATOR", 1);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playGladiatorLine(0x6cd, 0);
+	lineError = runtime.playDialogueLineWithVariant(0x6cd, "GLADIATOR", 0);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
diff --git a/engines/harvester/npc/gladiator_dialogue.h b/engines/harvester/npc/gladiator_dialogue.h
index bc6026d86eb..cb527c2e134 100644
--- a/engines/harvester/npc/gladiator_dialogue.h
+++ b/engines/harvester/npc/gladiator_dialogue.h
@@ -28,7 +28,6 @@ namespace Harvester {
 
 class GladiatorDialogueHandler : public NpcDialogueHandler {
 public:
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {}
 	Common::Error handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) override;
diff --git a/engines/harvester/npc/hank_dialogue.cpp b/engines/harvester/npc/hank_dialogue.cpp
index eddf962cc83..8d1fd1393d1 100644
--- a/engines/harvester/npc/hank_dialogue.cpp
+++ b/engines/harvester/npc/hank_dialogue.cpp
@@ -178,10 +178,6 @@ static const DialogueLineEntry kHankResponse0EdTopicLines[] = {
 
 } // End of namespace
 
-bool HankDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase("HANK");
-}
-
 Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	const bool europeanRelease = runtime.engine().isEuropeanRelease();
@@ -191,17 +187,8 @@ Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	int &hankTopicBufferLineIndex = state.currentTopicBufferLineIndex;
 	state.currentTopicBuffer.clear();
 	state.currentTopicBufferLineIndex = -1;
-	auto assignHankTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(hankTopicBuffer, hankTopicBufferLineIndex,
-			responseLineIndex, "Hank topic buffer");
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
-	auto playLine = [&](int wavId, const char *speakerId = "HANK",
-			int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, speakerId, headVariant);
-	};
+	DialogueTopicBuffer topics(runtime, hankTopicBuffer, hankTopicBufferLineIndex, "Hank topic buffer");
+
 	auto hidePresentedInventoryItem = [&]() {
 		(void)runtime.startupScript().setRuntimeObjectVisible(
 			kInventoryOwnerName, usedItemName, false);
@@ -274,19 +261,19 @@ Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			lineError = runtime.playDialogueLine(0x70f, "HANK");
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHankTopicBuffer(0xc8);
+			topics.assign(0xc8);
 			break;
 		case 2:
 			lineError = runtime.playDialogueLine(0x715, "HANK");
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHankTopicBuffer(0xc9);
+			topics.assign(0xc9);
 			break;
 		case 3:
 			lineError = runtime.playDialogueLine(0x71c, "HANK");
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHankTopicBuffer(0xca);
+			topics.assign(0xca);
 			break;
 		default:
 			break;
@@ -310,11 +297,9 @@ Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				if (responseError.getCode() != Common::kNoError)
 					return responseError;
 				if (responseIndex == 1) {
-					lineError = playSequence(
-						kHankRangshotResponse1Lines, ARRAYSIZE(kHankRangshotResponse1Lines));
+					lineError = runtime.playDialogueEntrySequence(kHankRangshotResponse1Lines, ARRAYSIZE(kHankRangshotResponse1Lines));
 				} else if (responseIndex == 2) {
-					lineError = playSequence(
-						kHankRangshotResponse2Lines, ARRAYSIZE(kHankRangshotResponse2Lines));
+					lineError = runtime.playDialogueEntrySequence(kHankRangshotResponse2Lines, ARRAYSIZE(kHankRangshotResponse2Lines));
 				}
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
@@ -408,8 +393,8 @@ Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (!skipHankFollowupBranches && runtime.startupScript().getFlagValue("BURNED_TV_STATION") &&
 			!state.burnedTvStationShown) {
 		state.burnedTvStationShown = true;
-		Common::Error lineError = playSequence(
-			kHankBurnedTvStationLines, ARRAYSIZE(kHankBurnedTvStationLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kHankBurnedTvStationLines,
+			ARRAYSIZE(kHankBurnedTvStationLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -519,8 +504,7 @@ Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			lineError = runtime.playDialogueLine(0xa20, "HANK");
 			break;
 		case 2:
-			lineError = playSequence(
-				kHankKarinFoundDeadResponse2Lines,
+			lineError = runtime.playDialogueEntrySequence(kHankKarinFoundDeadResponse2Lines,
 				ARRAYSIZE(kHankKarinFoundDeadResponse2Lines));
 			break;
 		default:
@@ -563,35 +547,33 @@ Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 
 		if (runtime.matchesResponseLine(selectedTopic, europeanRelease ? 0x30f : 0xd2)) {
-			Common::Error lineError = playSequence(
-				kHankMomTopicLines, ARRAYSIZE(kHankMomTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankMomTopicLines, ARRAYSIZE(kHankMomTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			runtime.setActiveSpeakerPortrait("HANK", 0);
-			assignHankTopicBuffer(0xd5);
+			topics.assign(0xd5);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xd6)) {
-			Common::Error lineError = playSequence(
-				kHankSteveTopicLines, ARRAYSIZE(kHankSteveTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankSteveTopicLines, ARRAYSIZE(kHankSteveTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHankTopicBuffer(0xd7);
+			topics.assign(0xd7);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xd8) ||
 				runtime.matchesResponseLine(selectedTopic, 0xd9)) {
-			Common::Error lineError = playSequence(
-				kHankLousyRatTopicLines, ARRAYSIZE(kHankLousyRatTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankLousyRatTopicLines,
+				ARRAYSIZE(kHankLousyRatTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHankTopicBuffer(0xda);
+			topics.assign(0xda);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xdb) ||
 				runtime.matchesResponseLine(selectedTopic, 0xdc)) {
-			Common::Error lineError = playSequence(
-				kHankLightingOutTopicLines, ARRAYSIZE(kHankLightingOutTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankLightingOutTopicLines,
+				ARRAYSIZE(kHankLightingOutTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -600,67 +582,64 @@ Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1) {
-				lineError = playLine(0x79d);
+				lineError = runtime.playDialogueLineWithVariant(0x79d, "HANK", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignHankTopicBuffer(0xde);
+				topics.assign(0xde);
 				continue;
 			}
 			if (responseIndex == 2) {
-				lineError = playLine(0x7a4);
+				lineError = runtime.playDialogueLineWithVariant(0x7a4, "HANK", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignHankTopicBuffer(0xdf);
+				topics.assign(0xdf);
 				continue;
 			}
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xe0)) {
-			Common::Error lineError = playLine(0x7ad);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x7ad, "HANK", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xe1)) {
-			Common::Error lineError = playSequence(
-				kHankDaddyTopicLines, ARRAYSIZE(kHankDaddyTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankDaddyTopicLines, ARRAYSIZE(kHankDaddyTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHankTopicBuffer(0xe2);
+			topics.assign(0xe2);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xe3)) {
-			Common::Error lineError = playSequence(
-				kHankGirlTopicLines, ARRAYSIZE(kHankGirlTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankGirlTopicLines, ARRAYSIZE(kHankGirlTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHankTopicBuffer(0xe4);
+			topics.assign(0xe4);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xe5)) {
-			Common::Error lineError = playLine(0x7f4);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x7f4, "HANK", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xe6)) {
-			Common::Error lineError = playSequence(
-				kHankTvTopicIntroLines, ARRAYSIZE(kHankTvTopicIntroLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankTvTopicIntroLines,
+				ARRAYSIZE(kHankTvTopicIntroLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC021FstPath);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playSequence(
-				kHankTvTopicOutroLines, ARRAYSIZE(kHankTvTopicOutroLines));
+			lineError = runtime.playDialogueEntrySequence(kHankTvTopicOutroLines, ARRAYSIZE(kHankTvTopicOutroLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHankTopicBuffer(0xe8);
+			topics.assign(0xe8);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xe9)) {
-			Common::Error lineError = playSequence(
-				kHankCowboyShowTopicLines, ARRAYSIZE(kHankCowboyShowTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankCowboyShowTopicLines,
+				ARRAYSIZE(kHankCowboyShowTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -669,25 +648,24 @@ Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1) {
-				lineError = playLine(0x856);
+				lineError = runtime.playDialogueLineWithVariant(0x856, "HANK", 0);
 			} else if (responseIndex == 2) {
-				lineError = playLine(0x85a);
+				lineError = runtime.playDialogueLineWithVariant(0x85a, "HANK", 0);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xeb)) {
-			Common::Error lineError = playSequence(
-				kHankSickTopicLines, ARRAYSIZE(kHankSickTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankSickTopicLines, ARRAYSIZE(kHankSickTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHankTopicBuffer(0xec);
+			topics.assign(0xec);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xed)) {
-			Common::Error lineError = playSequence(
-				kHankResponse0EdTopicLines, ARRAYSIZE(kHankResponse0EdTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankResponse0EdTopicLines,
+				ARRAYSIZE(kHankResponse0EdTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
@@ -695,8 +673,7 @@ Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesResponseLine(selectedTopic, 0xee) ||
 				runtime.matchesResponseLine(selectedTopic, 0xef) ||
 				runtime.matchesResponseLine(selectedTopic, 0xf0)) {
-			Common::Error lineError = playSequence(
-				kHankNewspaperFireSentinelTopicLines,
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankNewspaperFireSentinelTopicLines,
 				ARRAYSIZE(kHankNewspaperFireSentinelTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -704,16 +681,15 @@ Common::Error HankDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xf1)) {
 			sharedState.discussedLodgeTopic = true;
-			Common::Error lineError = playSequence(
-				kHankLodgeTopicLines, ARRAYSIZE(kHankLodgeTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kHankLodgeTopicLines, ARRAYSIZE(kHankLodgeTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0xf2))
-			return playLine(0x8dc);
+			return runtime.playDialogueLineWithVariant(0x8dc, "HANK", 0);
 
-		Common::Error lineError = playLine(0xa32);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0xa32, "HANK", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		continue;
diff --git a/engines/harvester/npc/hank_dialogue.h b/engines/harvester/npc/hank_dialogue.h
index b6e3a3028d0..fb21ef45799 100644
--- a/engines/harvester/npc/hank_dialogue.h
+++ b/engines/harvester/npc/hank_dialogue.h
@@ -44,7 +44,6 @@ public:
 		int currentTopicBufferLineIndex = -1;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = HankRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.pendingInitialConversation);
diff --git a/engines/harvester/npc/herrill_dialogue.cpp b/engines/harvester/npc/herrill_dialogue.cpp
index 5ff80e517ed..d5b32dece1c 100644
--- a/engines/harvester/npc/herrill_dialogue.cpp
+++ b/engines/harvester/npc/herrill_dialogue.cpp
@@ -30,9 +30,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kHerrillNpc = "HERRILL";
-static const char *const kHerrillLogNpc = "HERRILL_LOG";
-static const char *const kPcSpeaker = "PC";
 static const char *const kGrandMuckNpc = "GRAND_MUCK";
 static const int kGrandMuckDeathDamageType = 1;
 
@@ -40,26 +37,21 @@ static const int kHerrillGascanResponseLine = 0x280;
 static const int kHerrillTopicBufferResponseLine = 0x281;
 
 static const DialogueLineEntry kHerrillIntroLines[] = {
-	{ 0x2f59, kHerrillNpc, 1 },
-	{ 0x2f5e, kPcSpeaker, 0 },
-	{ 0x2f62, kHerrillNpc, 1 },
-	{ 0x1654, kPcSpeaker, 0 },
-	{ 0x2f6e, kHerrillNpc, 0 }
+	{ 0x2f59, "HERRILL", 1 },
+	{ 0x2f5e, "PC", 0 },
+	{ 0x2f62, "HERRILL", 1 },
+	{ 0x1654, "PC", 0 },
+	{ 0x2f6e, "HERRILL", 0 }
 };
 
 static const DialogueLineEntry kHerrillGascanIntroLines[] = {
-	{ 0x30cf, kHerrillNpc, 1 },
-	{ 0x30d3, kPcSpeaker, 0 },
-	{ 0x30d8, kHerrillNpc, 0 }
+	{ 0x30cf, "HERRILL", 1 },
+	{ 0x30d3, "PC", 0 },
+	{ 0x30d8, "HERRILL", 0 }
 };
 
 } // End of namespace
 
-bool HerrillDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kHerrillNpc) ||
-		npcName.equalsIgnoreCase(kHerrillLogNpc);
-}
-
 Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	HerrillRoomDialogueState &state = _state;
@@ -68,22 +60,8 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	state.currentTopicBuffer.clear();
 	state.currentTopicBufferLineIndex = -1;
 
-	auto assignHerrillTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(herrillTopicBuffer, herrillTopicBufferLineIndex,
-			responseLineIndex, "Herrill topic buffer");
-	};
-	auto playHerrillLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kHerrillNpc, headVariant);
-	};
-	auto playHerrillLogLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kHerrillLogNpc, headVariant);
-	};
-	auto playPcLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kPcSpeaker, headVariant);
-	};
-	auto playGrandMuckLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kGrandMuckNpc, headVariant);
-	};
+	DialogueTopicBuffer topics(runtime, herrillTopicBuffer, herrillTopicBufferLineIndex, "Herrill topic buffer");
+
 	auto queueLiveNpcDeathTransition = [&](const char *npcName) {
 		InteractionResult interaction;
 		if (runtime.startupScript().queueRuntimeNpcDeathOrMonsterfy(
@@ -117,26 +95,26 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	if (runtime.startupScript().getFlagValue("HERRILL_IN_LODGE")) {
 		if (!runtime.startupScript().getFlagValue("PC_TALKED_TO_HERRILL")) {
-			Common::Error lineError = playHerrillLogLine(0x31ca, 1);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x31ca, "HERRILL_LOG", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playPcLine(0x31ce, 4);
+			lineError = runtime.playDialogueLineWithVariant(0x31ce, "PC", 4);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playHerrillLogLine(0x31d2);
+			lineError = runtime.playDialogueLineWithVariant(0x31d2, "HERRILL_LOG", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playPcLine(0x31da);
+			lineError = runtime.playDialogueLineWithVariant(0x31da, "PC", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playGrandMuckLine(0x31de, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x31de, kGrandMuckNpc, 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			if (shownWhaleyHerrillPhoto()) {
-				lineError = playGrandMuckLine(0x31e0, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x31e0, kGrandMuckNpc, 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playHerrillLogLine(0x31e2, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x31e2, "HERRILL_LOG", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
@@ -155,7 +133,7 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	bool showGascanResponse = false;
 	if (!usedItemName.empty()) {
 		if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
-			return playHerrillLine(0x309f, 2);
+			return runtime.playDialogueLineWithVariant(0x309f, "HERRILL", 2);
 		}
 		const bool isCasketPhoto = usedItemName.equalsIgnoreCase("CASKET_PHOTO") ||
 			usedItemName.equalsIgnoreCase("CASKET_PHOTOCOPY");
@@ -170,7 +148,7 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				sharedState.discussedLedgerEvidence = 1;
 				(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownLedgersToAnyone, true);
 			}
-			Common::Error lineError = playHerrillLine(0x30a6);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x30a6, "HERRILL", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			int responseIndex = 0;
@@ -178,9 +156,9 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1)
-				return playHerrillLine(0x30b1, 2);
+				return runtime.playDialogueLineWithVariant(0x30b1, "HERRILL", 2);
 			if (responseIndex == 2)
-				return playHerrillLine(0x30b6, 2);
+				return runtime.playDialogueLineWithVariant(0x30b6, "HERRILL", 2);
 			return Common::kNoError;
 		}
 		if (usedItemName.equalsIgnoreCase("GASCAN")) {
@@ -190,9 +168,9 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownEvidenceSheriffOwns, true);
 			sharedState.discussedTvDeedEvidence = 1;
 			const DialogueLineEntry lines[] = {
-				{ 0x3188, kHerrillNpc, 0 },
-				{ 0x318c, kPcSpeaker, 0 },
-				{ 0x3191, kHerrillNpc, 2 }
+				{ 0x3188, "HERRILL", 0 },
+				{ 0x318c, "PC", 0 },
+				{ 0x3191, "HERRILL", 2 }
 			};
 			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
@@ -200,10 +178,10 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			sharedState.dialogueStateD2eec = true;
 			if (!state.dialogueStateD2ce4)
 				return Common::kNoError;
-			lineError = playHerrillLine(0x3198, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x3198, "HERRILL", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			return playHerrillLine(0x319d, 2);
+			return runtime.playDialogueLineWithVariant(0x319d, "HERRILL", 2);
 		} else if (usedItemName.equalsIgnoreCase("NOTE") ||
 				usedItemName.equalsIgnoreCase("NOTE_PHOTOCOPY") ||
 				usedItemName.equalsIgnoreCase("CHECKBOOK") ||
@@ -211,16 +189,16 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownEvidenceOfBlackmail, true);
 			sharedState.discussedNoteCheckbookEvidence = 1;
 			const DialogueLineEntry lines[] = {
-				{ 0x30bf, kHerrillNpc, 0 },
-				{ 0x30c4, kPcSpeaker, 0 },
-				{ 0x30c8, kHerrillNpc, 0 }
+				{ 0x30bf, "HERRILL", 0 },
+				{ 0x30c4, "PC", 0 },
+				{ 0x30c8, "HERRILL", 0 }
 			};
 			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			showGascanResponse = true;
 		} else {
-			return playHerrillLine(0x3099);
+			return runtime.playDialogueLineWithVariant(0x3099, "HERRILL", 0);
 		}
 	}
 
@@ -234,7 +212,7 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			state.gascanPresented = false;
 		}
 		if (!sharedState.dialogueStateD2e98)
-			return playPcLine(0x3123);
+			return runtime.playDialogueLineWithVariant(0x3123, "PC", 0);
 
 		state.dialogueStateD2ce4 = true;
 		int responseIndex = 0;
@@ -243,45 +221,45 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playHerrillLine(0x30e7);
+			lineError = runtime.playDialogueLineWithVariant(0x30e7, "HERRILL", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			if (sharedState.dialogueStateD2eb0) {
-				lineError = playPcLine(0x30eb);
+				lineError = runtime.playDialogueLineWithVariant(0x30eb, "PC", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 			if (sharedState.boyleGascanApplicationState) {
-				lineError = playHerrillLine(0x30f4);
+				lineError = runtime.playDialogueLineWithVariant(0x30f4, "HERRILL", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 
 			if (runtime.startupScript().getFlagValue("HAVE_LODGE_APP")) {
-				lineError = playPcLine(0x30fa);
+				lineError = runtime.playDialogueLineWithVariant(0x30fa, "PC", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playHerrillLine(0x30ff, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x30ff, "HERRILL", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				if (sharedState.dialogueStateD2eec) {
-					lineError = playHerrillLine(0x3104);
+					lineError = runtime.playDialogueLineWithVariant(0x3104, "HERRILL", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				}
-				return playHerrillLine(0x3109, 2);
+				return runtime.playDialogueLineWithVariant(0x3109, "HERRILL", 2);
 			}
 
-			lineError = playHerrillLine(0x30f0);
+			lineError = runtime.playDialogueLineWithVariant(0x30f0, "HERRILL", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playPcLine(0x3110);
+			lineError = runtime.playDialogueLineWithVariant(0x3110, "PC", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			return playHerrillLine(0x3114, 3);
+			return runtime.playDialogueLineWithVariant(0x3114, "HERRILL", 3);
 		}
 		if (responseIndex == 2)
-			return playHerrillLine(0x311b, 1);
+			return runtime.playDialogueLineWithVariant(0x311b, "HERRILL", 1);
 
 		return Common::kNoError;
 	}
@@ -292,9 +270,9 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			kHerrillIntroLines, ARRAYSIZE(kHerrillIntroLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		assignHerrillTopicBuffer(kHerrillTopicBufferResponseLine);
+		topics.assign(kHerrillTopicBufferResponseLine);
 	} else {
-		Common::Error lineError = playHerrillLine(0x2ffb);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x2ffb, "HERRILL", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -302,34 +280,34 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED") &&
 			!state.stephMidgamePlayedShown) {
 		state.stephMidgamePlayedShown = true;
-		Common::Error lineError = playHerrillLine(0x3004, 3);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3004, "HERRILL", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (runtime.startupScript().getCurrentStoryDayIndex() == 5 &&
 			!state.dayFiveShown) {
 		state.dayFiveShown = true;
-		Common::Error lineError = playHerrillLine(0x300d, 1);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x300d, "HERRILL", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (runtime.startupScript().getFlagValue("SCRATCHED_TUCKER") &&
 			!state.scratchedTuckerShown) {
 		state.scratchedTuckerShown = true;
-		Common::Error lineError = playHerrillLine(0x3019, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3019, "HERRILL", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playHerrillLine(0x3022, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x3022, "HERRILL", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (runtime.startupScript().getFlagValue("BARBER_POLE_STOLEN") &&
 			!state.barberPoleStolenShown) {
 		state.barberPoleStolenShown = true;
-		Common::Error lineError = playHerrillLine(0x3029);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3029, "HERRILL", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playHerrillLine(0x302f, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x302f, "HERRILL", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -337,50 +315,52 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.karinKidnapedShown) {
 		state.karinKidnapedShown = true;
 		sharedState.karinKidnapedDialogueState = true;
-		Common::Error lineError = playHerrillLine(0x3041);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3041, "HERRILL", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (runtime.startupScript().getFlagValue("KARIN_FOUND_ALIVE") &&
 			!state.karinFoundAliveShown) {
 		state.karinFoundAliveShown = true;
-		Common::Error lineError = playHerrillLine(0x304c, 1);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x304c, "HERRILL", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = shownWhaleyHerrillPhoto() ? playHerrillLine(0x3050, 2) : playHerrillLine(0x3057, 1);
+		lineError = shownWhaleyHerrillPhoto()
+			? runtime.playDialogueLineWithVariant(0x3050, "HERRILL", 2)
+			: runtime.playDialogueLineWithVariant(0x3057, "HERRILL", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (runtime.startupScript().getFlagValue("KARIN_FOUND_DEAD") &&
 			!state.karinFoundDeadShown) {
 		state.karinFoundDeadShown = true;
-		Common::Error lineError = playHerrillLine(0x305e);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x305e, "HERRILL", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playPcLine(0x461b, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x461b, "PC", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED")) {
-			lineError = playHerrillLine(0x3067, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x3067, "HERRILL", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 		lineError = shownWhaleyHerrillPhoto()
-			? playHerrillLine(0x306b, 2)
-			: playHerrillLine(0x3073, 3);
+			? runtime.playDialogueLineWithVariant(0x306b, "HERRILL", 2)
+			: runtime.playDialogueLineWithVariant(0x3073, "HERRILL", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (runtime.startupScript().getFlagValue("DINER_BURNED") &&
 			!state.dinerBurnedShown) {
 		state.dinerBurnedShown = true;
-		Common::Error lineError = playHerrillLine(0x307d, 3);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x307d, "HERRILL", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (shownWhaleyHerrillPhoto() && !state.whaleyHerrillFollowupShown) {
 		state.whaleyHerrillFollowupShown = true;
-		Common::Error lineError = playHerrillLine(0x31a6, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x31a6, "HERRILL", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		int responseIndex = 0;
@@ -388,14 +368,14 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (responseError.getCode() != Common::kNoError)
 			return responseError;
 		if (responseIndex == 1) {
-			lineError = playHerrillLine(0x31b1, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x31b1, "HERRILL", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			state.dialogueSuppressed = true;
 			return Common::kNoError;
 		}
 		if (responseIndex == 2) {
-			lineError = playHerrillLine(0x31b9, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x31b9, "HERRILL", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			state.requiresEvidenceForDialogue = true;
@@ -412,89 +392,89 @@ Common::Error HerrillDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (selectedTopic.empty())
 			return Common::kNoError;
 		if (runtime.matchesResponseLine(selectedTopic, 0x283)) {
-			return playHerrillLine(0x2ff4);
+			return runtime.playDialogueLineWithVariant(0x2ff4, "HERRILL", 0);
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x284)) {
 			const DialogueLineEntry lines[] = {
-				{ 0x2f7b, kPcSpeaker, 2 },
-				{ 0x2f7f, kHerrillNpc, 0 },
-				{ 0x2f80, kHerrillNpc, 0 },
-				{ 0x2f81, kHerrillNpc, 0 }
+				{ 0x2f7b, "PC", 2 },
+				{ 0x2f7f, "HERRILL", 0 },
+				{ 0x2f80, "HERRILL", 0 },
+				{ 0x2f81, "HERRILL", 0 }
 			};
 			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHerrillTopicBuffer(0x285);
+			topics.assign(0x285);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x286)) {
 			const DialogueLineEntry lines[] = {
-				{ 0x2f8f, kHerrillNpc, 0 },
-				{ 0x2f90, kHerrillNpc, 0 },
-				{ 0x2f91, kHerrillNpc, 1 }
+				{ 0x2f8f, "HERRILL", 0 },
+				{ 0x2f90, "HERRILL", 0 },
+				{ 0x2f91, "HERRILL", 1 }
 			};
 			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHerrillTopicBuffer(0x287);
+			topics.assign(0x287);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x288)) {
 			const DialogueLineEntry lines[] = {
-				{ 0x2f9e, kHerrillNpc, 1 },
-				{ 0x2fa3, kPcSpeaker, 2 },
-				{ 0x2fa7, kHerrillNpc, 1 }
+				{ 0x2f9e, "HERRILL", 1 },
+				{ 0x2fa3, "PC", 2 },
+				{ 0x2fa7, "HERRILL", 1 }
 			};
 			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHerrillTopicBuffer(0x289);
+			topics.assign(0x289);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x28a)) {
 			const DialogueLineEntry lines[] = {
-				{ 0x2fb2, kPcSpeaker, 0 },
-				{ 0x2fb6, kHerrillNpc, 0 },
-				{ 0x2fbe, kHerrillNpc, 1 },
-				{ 0x2fc2, kHerrillNpc, 1 }
+				{ 0x2fb2, "PC", 0 },
+				{ 0x2fb6, "HERRILL", 0 },
+				{ 0x2fbe, "HERRILL", 1 },
+				{ 0x2fc2, "HERRILL", 1 }
 			};
 			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHerrillTopicBuffer(0x28b);
+			topics.assign(0x28b);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x28c)) {
 			const DialogueLineEntry lines[] = {
-				{ 0x2fcd, kPcSpeaker, 0 },
-				{ 0x2fd2, kHerrillNpc, 0 }
+				{ 0x2fcd, "PC", 0 },
+				{ 0x2fd2, "HERRILL", 0 }
 			};
 			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHerrillTopicBuffer(0x28d);
+			topics.assign(0x28d);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x28e) ||
 				runtime.matchesResponseLine(selectedTopic, 0x28f)) {
 			sharedState.discussedLodgeTopic = true;
-			Common::Error lineError = playHerrillLine(0x2fdd);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x2fdd, "HERRILL", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHerrillTopicBuffer(0x290);
+			topics.assign(0x290);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x291)) {
-			Common::Error lineError = playHerrillLine(0x2fe9, 1);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x2fe9, "HERRILL", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignHerrillTopicBuffer(0x292);
+			topics.assign(0x292);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x293))
-			return playHerrillLine(0x2ff4);
+			return runtime.playDialogueLineWithVariant(0x2ff4, "HERRILL", 0);
 
-		Common::Error lineError = playHerrillLine(0x3099);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x3099, "HERRILL", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
diff --git a/engines/harvester/npc/herrill_dialogue.h b/engines/harvester/npc/herrill_dialogue.h
index 0a68d9356de..5f174dcf1f6 100644
--- a/engines/harvester/npc/herrill_dialogue.h
+++ b/engines/harvester/npc/herrill_dialogue.h
@@ -47,7 +47,6 @@ public:
 		int currentTopicBufferLineIndex = -1;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = HerrillRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.introPending);
diff --git a/engines/harvester/npc/inquisitor_dialogue.cpp b/engines/harvester/npc/inquisitor_dialogue.cpp
index 7e7e50a739a..f4ebd223706 100644
--- a/engines/harvester/npc/inquisitor_dialogue.cpp
+++ b/engines/harvester/npc/inquisitor_dialogue.cpp
@@ -28,17 +28,12 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kInquisitorNpc = "INQUISITOR";
 static const char *const kPainSolvedFlag = "PAIN_SOLVED";
 static const char *const kStartInquisitorTimerActionTag = "START_INQ_TIM";
 static const char *const kTorch2FstPath = "GRAPHIC/FST/TORCH2.FST";
 
 } // End of namespace
 
-bool InquisitorDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kInquisitorNpc);
-}
-
 Common::Error InquisitorDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
 	if (runtime.startupScript().getFlagValue(kPainSolvedFlag) || !_state.talkStatePending)
@@ -46,19 +41,15 @@ Common::Error InquisitorDialogueHandler::handleDialogue(DialogueRuntime &runtime
 
 	_state.talkStatePending = false;
 
-	auto playInquisitorLine = [&](int wavId) -> Common::Error {
-		return runtime.playDialogueLine(wavId, kInquisitorNpc);
-	};
-
-	Common::Error lineError = playInquisitorLine(0x18e7);
+	Common::Error lineError = runtime.playDialogueLine(0x18e7, "INQUISITOR");
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playInquisitorLine(0x18e8);
+	lineError = runtime.playDialogueLine(0x18e8, "INQUISITOR");
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playInquisitorLine(0x18e9);
+	lineError = runtime.playDialogueLine(0x18e9, "INQUISITOR");
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -66,7 +57,7 @@ Common::Error InquisitorDialogueHandler::handleDialogue(DialogueRuntime &runtime
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playInquisitorLine(0x18f8);
+	lineError = runtime.playDialogueLine(0x18f8, "INQUISITOR");
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
diff --git a/engines/harvester/npc/inquisitor_dialogue.h b/engines/harvester/npc/inquisitor_dialogue.h
index dd91094d0e5..1f3d1298fa3 100644
--- a/engines/harvester/npc/inquisitor_dialogue.h
+++ b/engines/harvester/npc/inquisitor_dialogue.h
@@ -32,7 +32,6 @@ public:
 		bool talkStatePending = true;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = InquisitorRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.talkStatePending);
diff --git a/engines/harvester/npc/jimmy_dialogue.cpp b/engines/harvester/npc/jimmy_dialogue.cpp
index a1bde6cfb31..37f74137387 100644
--- a/engines/harvester/npc/jimmy_dialogue.cpp
+++ b/engines/harvester/npc/jimmy_dialogue.cpp
@@ -29,8 +29,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kJimmyNpc = "JIMMY";
-static const char *const kPcSpeaker = "PC";
 static const char *const kRahRoomName = "RAH";
 static const char *const kJimmyFirstMeetingFlag = "PAPER_CHK_1";
 static const char *const kGivenPaperTodayFlag = "GIVEN_PAPER_TODAY";
@@ -40,74 +38,57 @@ static const int kJimmyThirdResponseLineIndex = 0xf7;
 static const int kJimmySneakersResponseLineIndex = 0xf8;
 
 static const DialogueLineEntry kJimmyPaperHandoffLines[] = {
-	{ 0x4a4c, kJimmyNpc, 1 },
-	{ 0x4a52, kPcSpeaker, 0 }
+	{ 0x4a4c, "JIMMY", 1 },
+	{ 0x4a52, "PC", 0 }
 };
 
 static const DialogueLineEntry kJimmyFirstMeetingFollowupLines[] = {
-	{ 0x4a6b, kJimmyNpc, 0 },
-	{ 0x4a70, kPcSpeaker, 0 },
-	{ 0x4a74, kJimmyNpc, 0 },
-	{ 0x4a7a, kJimmyNpc, 0 }
+	{ 0x4a6b, "JIMMY", 0 },
+	{ 0x4a70, "PC", 0 },
+	{ 0x4a74, "JIMMY", 0 },
+	{ 0x4a7a, "JIMMY", 0 }
 };
 
 static const DialogueLineEntry kJimmySecondResponsePreludeLines[] = {
-	{ 0x4a8a, kPcSpeaker, 0 },
-	{ 0x4a8e, kJimmyNpc, 1 }
+	{ 0x4a8a, "PC", 0 },
+	{ 0x4a8e, "JIMMY", 1 }
 };
 
 static const DialogueLineEntry kJimmyNoSneakersLines[] = {
-	{ 0x4aa6, kJimmyNpc, 0 },
-	{ 0x4aaa, kPcSpeaker, 0 },
-	{ 0x4aae, kJimmyNpc, 2 }
+	{ 0x4aa6, "JIMMY", 0 },
+	{ 0x4aaa, "PC", 0 },
+	{ 0x4aae, "JIMMY", 2 }
 };
 
 static const DialogueLineEntry kJimmyLedgersAndCorpseEvidenceLines[] = {
-	{ 0x4b00, kJimmyNpc, 0 },
-	{ 0x4b04, kPcSpeaker, 0 },
-	{ 0x4b08, kJimmyNpc, 0 },
-	{ 0x4b0c, kPcSpeaker, 0 },
-	{ 0x4b10, kJimmyNpc, 0 },
-	{ 0x4b15, kPcSpeaker, 0 },
-	{ 0x4b19, kJimmyNpc, 0 }
+	{ 0x4b00, "JIMMY", 0 },
+	{ 0x4b04, "PC", 0 },
+	{ 0x4b08, "JIMMY", 0 },
+	{ 0x4b0c, "PC", 0 },
+	{ 0x4b10, "JIMMY", 0 },
+	{ 0x4b15, "PC", 0 },
+	{ 0x4b19, "JIMMY", 0 }
 };
 
 static const DialogueLineEntry kJimmyBlackmailEvidenceLines[] = {
-	{ 0x4b21, kJimmyNpc, 0 },
-	{ 0x4b25, kPcSpeaker, 0 },
-	{ 0x4b29, kJimmyNpc, 0 },
-	{ 0x4b2d, kPcSpeaker, 0 },
-	{ 0x4b31, kJimmyNpc, 1 }
+	{ 0x4b21, "JIMMY", 0 },
+	{ 0x4b25, "PC", 0 },
+	{ 0x4b29, "JIMMY", 0 },
+	{ 0x4b2d, "PC", 0 },
+	{ 0x4b31, "JIMMY", 1 }
 };
 
 } // End of namespace
 
-bool JimmyDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase("JIMMY");
-}
-
 Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	JimmyRoomDialogueState &state = _state;
 	Common::Error lineError = Common::kNoError;
 
-	auto playJimmyLine = [&](int wavId, int headVariant) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kJimmyNpc, headVariant);
-	};
-	auto playJimmySequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
 	auto hasInventoryItem = [&](const char *objectName) {
 		return runtime.startupScript().isObjectInInventory(objectName);
 	};
-	auto executeActionTagIfSet = [&](const char *tag) {
-		InteractionResult interaction;
-		if (!runtime.executeActionTag(tag, interaction))
-			return;
 
-		runtime.applyImmediateDialogueInteractionEffects(interaction);
-		runtime.queueDialogueInteractionIfNeeded(interaction);
-	};
 	auto restoreItemToRah = [&](const char *objectName) {
 		(void)runtime.startupScript().resetRuntimeObjectToInitialState(objectName);
 		(void)runtime.startupScript().setRuntimeObjectVisible(kRahRoomName, objectName, true);
@@ -120,8 +101,8 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (state.paperHandoffStateSet)
 			return Common::kNoError;
 
-		Common::Error preludeError = playJimmySequence(
-			kJimmyPaperHandoffLines, ARRAYSIZE(kJimmyPaperHandoffLines));
+		Common::Error preludeError = runtime.playDialogueEntrySequence(kJimmyPaperHandoffLines,
+			ARRAYSIZE(kJimmyPaperHandoffLines));
 		if (preludeError.getCode() != Common::kNoError)
 			return preludeError;
 
@@ -136,7 +117,7 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (responseIndex == 1) {
 			if (hasInventoryItem("SNEAKERS")) {
-				Common::Error lineError = playJimmyLine(0x4a9e, 1);
+				Common::Error lineError = runtime.playDialogueLineWithVariant(0x4a9e, "JIMMY", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -144,10 +125,10 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return Common::kNoError;
 			}
 
-			return playJimmySequence(kJimmyNoSneakersLines, ARRAYSIZE(kJimmyNoSneakersLines));
+			return runtime.playDialogueEntrySequence(kJimmyNoSneakersLines, ARRAYSIZE(kJimmyNoSneakersLines));
 		}
 		if (responseIndex == 2)
-			return playJimmyLine(0x4ab4, 1);
+			return runtime.playDialogueLineWithVariant(0x4ab4, "JIMMY", 1);
 
 		return Common::kNoError;
 	};
@@ -158,11 +139,11 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			Common::Error lineError = playJimmyLine(0x4a86, 1);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x4a86, "JIMMY", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			lineError = playJimmySequence(kJimmySecondResponsePreludeLines,
+			lineError = runtime.playDialogueEntrySequence(kJimmySecondResponsePreludeLines,
 				ARRAYSIZE(kJimmySecondResponsePreludeLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -170,13 +151,13 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return runSneakersOfferMenu();
 		}
 		if (responseIndex == 2)
-			return playJimmyLine(0x4abc, 0);
+			return runtime.playDialogueLineWithVariant(0x4abc, "JIMMY", 0);
 
 		return Common::kNoError;
 	};
 	auto playJimmyNoItemFallback = [&]() -> Common::Error {
 		if (hasInventoryItem("SNEAKERS") && !hasInventoryItem("BROOMKEY")) {
-			Common::Error sneakersLineError = playJimmyLine(0x4ac3, 0);
+			Common::Error sneakersLineError = runtime.playDialogueLineWithVariant(0x4ac3, "JIMMY", 0);
 			if (sneakersLineError.getCode() != Common::kNoError)
 				return sneakersLineError;
 
@@ -185,29 +166,28 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return sneakersPromptError;
 		}
 		if (runtime.startupScript().getFlagValue("PAPER_CHK_4"))
-			return playJimmyLine(0x4ae2, 2);
+			return runtime.playDialogueLineWithVariant(0x4ae2, "JIMMY", 2);
 		if (runtime.startupScript().getFlagValue("PAPER_CHK_3"))
-			return playJimmyLine(0x4adb, 2);
+			return runtime.playDialogueLineWithVariant(0x4adb, "JIMMY", 2);
 		if (runtime.startupScript().getFlagValue("PAPER_CHK_2"))
-			return playJimmyLine(0x4ad4, 2);
+			return runtime.playDialogueLineWithVariant(0x4ad4, "JIMMY", 2);
 
-		return playJimmyLine(0x4b38, 0);
+		return runtime.playDialogueLineWithVariant(0x4b38, "JIMMY", 0);
 	};
 
 	if (!usedItemName.empty()) {
 		if (usedItemName.equalsIgnoreCase("NEWSPAPER")) {
 			(void)runtime.startupScript().setRuntimeFlagValue(kGivenPaperTodayFlag, true);
 			restoreItemToRah("NEWSPAPER");
-			executeActionTagIfSet("ACTV_HOUSE_EXIT");
+			runtime.executeAndQueueAction("ACTV_HOUSE_EXIT");
 
 			lineError = playPaperHandoffPreludeIfNeeded();
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			const bool playJimmyRevisitBark = runtime.getRandomNumber(1) == 0;
-			lineError = playJimmyLine(
-				playJimmyRevisitBark ? 0x4acc : 0x4a4b,
-				playJimmyRevisitBark ? 0 : 1);
+			lineError = runtime.playDialogueLineWithVariant(playJimmyRevisitBark ? 0x4acc : 0x4a4b,
+				"JIMMY", playJimmyRevisitBark ? 0 : 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -218,7 +198,7 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 		if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
 			sharedState.discussedWhaleyHerrillPhoto = 1;
-			return playJimmyLine(0x4af8, 1);
+			return runtime.playDialogueLineWithVariant(0x4af8, "JIMMY", 1);
 		}
 		if (((usedItemName.equalsIgnoreCase("LEDGER") ||
 					usedItemName.equalsIgnoreCase("LEDGER2")) &&
@@ -231,7 +211,7 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			} else {
 				sharedState.discussedLedgerEvidence = 1;
 			}
-			return playJimmySequence(kJimmyLedgersAndCorpseEvidenceLines,
+			return runtime.playDialogueEntrySequence(kJimmyLedgersAndCorpseEvidenceLines,
 				ARRAYSIZE(kJimmyLedgersAndCorpseEvidenceLines));
 		}
 		if (usedItemName.equalsIgnoreCase("NOTE") ||
@@ -239,11 +219,10 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				usedItemName.equalsIgnoreCase("CHECKBOOK") ||
 				usedItemName.equalsIgnoreCase("CHECKBOOK_PHOTOCOPY")) {
 			sharedState.discussedNoteCheckbookEvidence = 1;
-			return playJimmySequence(kJimmyBlackmailEvidenceLines,
-				ARRAYSIZE(kJimmyBlackmailEvidenceLines));
+			return runtime.playDialogueEntrySequence(kJimmyBlackmailEvidenceLines, ARRAYSIZE(kJimmyBlackmailEvidenceLines));
 		}
 		if (usedItemName.equalsIgnoreCase("SNEAKERS")) {
-			lineError = playJimmyLine(0x4a9e, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x4a9e, "JIMMY", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -251,7 +230,7 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return Common::kNoError;
 		}
 
-		return playJimmyLine(0x4af2, 0);
+		return runtime.playDialogueLineWithVariant(0x4af2, "JIMMY", 0);
 	}
 
 	if (state.firstNoItemLinePending) {
@@ -260,7 +239,7 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playJimmyLine(0x4a58, 0);
+		lineError = runtime.playDialogueLineWithVariant(0x4a58, "JIMMY", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -273,16 +252,17 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playJimmyLine(0x4a63, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x4a63, "JIMMY", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else if (responseIndex == 2) {
-			lineError = playJimmyLine(0x4a67, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x4a67, "JIMMY", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 
-		lineError = playJimmySequence(kJimmyFirstMeetingFollowupLines, ARRAYSIZE(kJimmyFirstMeetingFollowupLines));
+		lineError = runtime.playDialogueEntrySequence(kJimmyFirstMeetingFollowupLines,
+			ARRAYSIZE(kJimmyFirstMeetingFollowupLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
diff --git a/engines/harvester/npc/jimmy_dialogue.h b/engines/harvester/npc/jimmy_dialogue.h
index 47e723b79e7..5d563ff962a 100644
--- a/engines/harvester/npc/jimmy_dialogue.h
+++ b/engines/harvester/npc/jimmy_dialogue.h
@@ -33,7 +33,6 @@ public:
 		bool paperHandoffStateSet = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = JimmyRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.firstNoItemLinePending);
diff --git a/engines/harvester/npc/johnson_dialogue.cpp b/engines/harvester/npc/johnson_dialogue.cpp
index e9d35d8fed2..d49ed9ae3d6 100644
--- a/engines/harvester/npc/johnson_dialogue.cpp
+++ b/engines/harvester/npc/johnson_dialogue.cpp
@@ -29,8 +29,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kJohnsonNpc = "JOHNSON";
-static const char *const kPcSpeaker = "PC";
 
 static const int kJohnsonInitialTopicBufferLine = 0x176;
 static const int kJohnsonResponseTopicBufferLine = 0x17b;
@@ -43,59 +41,55 @@ static const int kJohnsonGenericReplyTopicLines[] = { 0x180, 0x181, 0x182 };
 static const int kJohnsonSecondGenericReplyTopicLines[] = { 0x184, 0x185 };
 
 static const DialogueLineEntry kJohnsonIntroLines[] = {
-	{ 0xa86, kJohnsonNpc, 1 },
-	{ 0xa8a, kPcSpeaker, 0 },
-	{ 0xa8e, kJohnsonNpc, 1 },
-	{ 0xa97, kPcSpeaker, 0 },
-	{ 0xa9b, kJohnsonNpc, 0 }
+	{ 0xa86, "JOHNSON", 1 },
+	{ 0xa8a, "PC", 0 },
+	{ 0xa8e, "JOHNSON", 1 },
+	{ 0xa97, "PC", 0 },
+	{ 0xa9b, "JOHNSON", 0 }
 };
 
 static const DialogueLineEntry kJohnsonWhaleyPhotoLines[] = {
-	{ 0xb75, kJohnsonNpc, 1 },
-	{ 0xb7b, kPcSpeaker, 1 },
-	{ 0xb80, kJohnsonNpc, 1 }
+	{ 0xb75, "JOHNSON", 1 },
+	{ 0xb7b, "PC", 1 },
+	{ 0xb80, "JOHNSON", 1 }
 };
 
 static const DialogueLineEntry kJohnsonLedgerLines[] = {
-	{ 0xba0, kJohnsonNpc, 1 },
-	{ 0xba4, kPcSpeaker, 0 },
-	{ 0xba8, kJohnsonNpc, 1 }
+	{ 0xba0, "JOHNSON", 1 },
+	{ 0xba4, "PC", 0 },
+	{ 0xba8, "JOHNSON", 1 }
 };
 
 static const DialogueLineEntry kJohnsonScratchedTuckerLines[] = {
-	{ 0xc25, kJohnsonNpc, 2 },
-	{ 0xc2b, kJohnsonNpc, 3 },
-	{ 0xc30, kJohnsonNpc, 2 }
+	{ 0xc25, "JOHNSON", 2 },
+	{ 0xc2b, "JOHNSON", 3 },
+	{ 0xc30, "JOHNSON", 2 }
 };
 
 static const DialogueLineEntry kJohnsonBoltOfClothLines[] = {
-	{ 0xc5f, kJohnsonNpc, 3 },
-	{ 0xc69, kJohnsonNpc, 3 }
+	{ 0xc5f, "JOHNSON", 3 },
+	{ 0xc69, "JOHNSON", 3 }
 };
 
 static const DialogueLineEntry kJohnsonStephMidgameLines[] = {
-	{ 0xbf0, kJohnsonNpc, 3 },
-	{ 0xbf1, kJohnsonNpc, 3 },
-	{ 0xbf2, kJohnsonNpc, 3 }
+	{ 0xbf0, "JOHNSON", 3 },
+	{ 0xbf1, "JOHNSON", 3 },
+	{ 0xbf2, "JOHNSON", 3 }
 };
 
 static const DialogueLineEntry kJohnsonKarinKidnapedLines[] = {
-	{ 0xbff, kJohnsonNpc, 0 },
-	{ 0xc06, kJohnsonNpc, 0 }
+	{ 0xbff, "JOHNSON", 0 },
+	{ 0xc06, "JOHNSON", 0 }
 };
 
 static const DialogueLineEntry kJohnsonBarberPoleStolenLines[] = {
-	{ 0xc46, kJohnsonNpc, 3 },
-	{ 0xc4d, kJohnsonNpc, 3 },
-	{ 0xc59, kJohnsonNpc, 3 }
+	{ 0xc46, "JOHNSON", 3 },
+	{ 0xc4d, "JOHNSON", 3 },
+	{ 0xc59, "JOHNSON", 3 }
 };
 
 } // End of namespace
 
-bool JohnsonDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase("JOHNSON");
-}
-
 Common::Error JohnsonDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	JohnsonRoomDialogueState &state = _state;
@@ -104,24 +98,18 @@ Common::Error JohnsonDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	state.currentTopicBuffer.clear();
 	state.currentTopicBufferLineIndex = -1;
 
-	auto assignJohnsonTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(johnsonTopicBuffer, johnsonTopicBufferLineIndex,
-			responseLineIndex, "Johnson topic buffer");
-	};
-	auto playJohnsonLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kJohnsonNpc, headVariant);
-	};
+	DialogueTopicBuffer topics(runtime, johnsonTopicBuffer, johnsonTopicBufferLineIndex, "Johnson topic buffer");
 
 	if (runtime.startupScript().getFlagValue("2ND_SCRATCH_TUCKER"))
-		return playJohnsonLine(0xbe6, 2);
+		return runtime.playDialogueLineWithVariant(0xbe6, "JOHNSON", 2);
 	if (runtime.startupScript().getFlagValue("TRY_TO_SCRATCH_TUCKER") &&
 			!state.tryToScratchTuckerShown) {
 		state.tryToScratchTuckerShown = true;
-		return playJohnsonLine(0xbe0, 2);
+		return runtime.playDialogueLineWithVariant(0xbe0, "JOHNSON", 2);
 	}
 	if (runtime.startupScript().getFlagValue("OPENING_MANHOLE_DAYTIME"))
 	{
-		Common::Error lineError = playJohnsonLine(0xbe0, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0xbe0, "JOHNSON", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -140,16 +128,16 @@ Common::Error JohnsonDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				usedItemName.equalsIgnoreCase("CASKET_PHOTOCOPY")) {
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownPhotoOfCorpse, true);
 			sharedState.discussedCasketPhotoEvidence = 1;
-			Common::Error lineError = playJohnsonLine(0xb89, 1);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0xb89, "JOHNSON", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			if (!sharedState.dialogueStateD2ebc)
 				return Common::kNoError;
 
 			const DialogueLineEntry followupLines[] = {
-				{ 0xb90, kPcSpeaker, 0 },
-				{ 0xb94, kJohnsonNpc, 0 },
-				{ 0xb99, kJohnsonNpc, 0 }
+				{ 0xb90, "PC", 0 },
+				{ 0xb94, "JOHNSON", 0 },
+				{ 0xb99, "JOHNSON", 0 }
 			};
 			return runtime.playDialogueEntrySequence(followupLines, ARRAYSIZE(followupLines));
 		}
@@ -167,16 +155,16 @@ Common::Error JohnsonDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				usedItemName.equalsIgnoreCase("CHECKBOOK_PHOTOCOPY")) {
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownEvidenceOfBlackmail, true);
 			sharedState.discussedNoteCheckbookEvidence = 1;
-			return playJohnsonLine(0xbb1, 1);
+			return runtime.playDialogueLineWithVariant(0xbb1, "JOHNSON", 1);
 		}
 		if (usedItemName.equalsIgnoreCase("TV_DEED") ||
 				usedItemName.equalsIgnoreCase("TV_DEED_PHOTOCOPY")) {
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownEvidenceSheriffOwns, true);
 			sharedState.discussedTvDeedEvidence = 1;
-			return playJohnsonLine(0xbce, 1);
+			return runtime.playDialogueLineWithVariant(0xbce, "JOHNSON", 1);
 		}
 
-		return playJohnsonLine(0xb6f);
+		return runtime.playDialogueLineWithVariant(0xb6f, "JOHNSON", 0);
 	}
 
 	if (runtime.startupScript().getFlagValue("SCRATCHED_TUCKER") &&
@@ -204,9 +192,9 @@ Common::Error JohnsonDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			sharedState.dialogueStateD2f04 = true;
-			assignJohnsonTopicBuffer(kJohnsonInitialTopicBufferLine);
+			topics.assign(kJohnsonInitialTopicBufferLine);
 		} else {
-			Common::Error lineError = playJohnsonLine(0xb62);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0xb62, "JOHNSON", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -232,10 +220,10 @@ Common::Error JohnsonDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("KARIN_FOUND_ALIVE") &&
 			!state.karinFoundAliveShown) {
 		state.karinFoundAliveShown = true;
-		Common::Error lineError = playJohnsonLine(0xc13, 1);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0xc13, "JOHNSON", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playJohnsonLine(0xc17);
+		lineError = runtime.playDialogueLineWithVariant(0xc17, "JOHNSON", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -250,28 +238,28 @@ Common::Error JohnsonDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("DINER_BURNED") &&
 			!state.dinerBurnedShown) {
 		state.dinerBurnedShown = true;
-		Common::Error lineError = playJohnsonLine(0xc6f, 3);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0xc6f, "JOHNSON", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (runtime.startupScript().getFlagValue("PC_ESCAPED_JAIL") &&
 			!state.escapedJailShown) {
 		state.escapedJailShown = true;
-		Common::Error lineError = playJohnsonLine(0xc77, 2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0xc77, "JOHNSON", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (runtime.startupScript().getFlagValue("GOT_REMAINS_FOR_LODGE") &&
 			!state.gotRemainsForLodgeShown) {
 		state.gotRemainsForLodgeShown = true;
-		Common::Error lineError = playJohnsonLine(0xc7f);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0xc7f, "JOHNSON", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 	if (runtime.startupScript().getFlagValue("BURNED_TV_STATION") &&
 			!state.burnedTvStationShown) {
 		state.burnedTvStationShown = true;
-		Common::Error lineError = playJohnsonLine(0xc8f);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0xc8f, "JOHNSON", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -286,11 +274,11 @@ Common::Error JohnsonDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return Common::kNoError;
 		if (runtime.matchesAnyResponseLine(selectedTopic, kJohnsonDirectExitTopicLines,
 					ARRAYSIZE(kJohnsonDirectExitTopicLines))) {
-			return playJohnsonLine(0xbda, 1);
+			return runtime.playDialogueLineWithVariant(0xbda, "JOHNSON", 1);
 		}
 		if (runtime.matchesAnyResponseLine(selectedTopic, kJohnsonIntroductionTopicLines,
 				ARRAYSIZE(kJohnsonIntroductionTopicLines))) {
-			Common::Error lineError = playJohnsonLine(0xaa6);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0xaa6, "JOHNSON", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -301,79 +289,79 @@ Common::Error JohnsonDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playJohnsonLine(0xab4, 2);
+				lineError = runtime.playDialogueLineWithVariant(0xab4, "JOHNSON", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else if (responseIndex == 2) {
-				lineError = playJohnsonLine(0xab9);
+				lineError = runtime.playDialogueLineWithVariant(0xab9, "JOHNSON", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 
 			const DialogueLineEntry followupLines[] = {
-				{ 0xabd, kJohnsonNpc, 0 },
-				{ 0xac3, kPcSpeaker, 0 },
-				{ 0xac7, kJohnsonNpc, 2 }
+				{ 0xabd, "JOHNSON", 0 },
+				{ 0xac3, "PC", 0 },
+				{ 0xac7, "JOHNSON", 2 }
 			};
 			lineError = runtime.playDialogueEntrySequence(followupLines, ARRAYSIZE(followupLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignJohnsonTopicBuffer(kJohnsonResponseTopicBufferLine);
+			topics.assign(kJohnsonResponseTopicBufferLine);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x17c)) {
 			const DialogueLineEntry lines[] = {
-				{ 0xb17, kJohnsonNpc, 0 },
-				{ 0xb1c, kPcSpeaker, 0 },
-				{ 0xb20, kJohnsonNpc, 1 }
+				{ 0xb17, "JOHNSON", 0 },
+				{ 0xb1c, "PC", 0 },
+				{ 0xb20, "JOHNSON", 1 }
 			};
 			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignJohnsonTopicBuffer(kJohnsonStephTopicBufferLine);
+			topics.assign(kJohnsonStephTopicBufferLine);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x17e)) {
-			Common::Error lineError = playJohnsonLine(0xb2d, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0xb2d, "JOHNSON", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignJohnsonTopicBuffer(kJohnsonKarinAliveTopicBufferLine);
+			topics.assign(kJohnsonKarinAliveTopicBufferLine);
 			continue;
 		}
 		if (runtime.matchesAnyResponseLine(selectedTopic, kJohnsonGenericReplyTopicLines,
 				ARRAYSIZE(kJohnsonGenericReplyTopicLines))) {
-			Common::Error lineError = playJohnsonLine(0xb3a);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0xb3a, "JOHNSON", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignJohnsonTopicBuffer(0x183);
+			topics.assign(0x183);
 			continue;
 		}
 		if (runtime.matchesAnyResponseLine(selectedTopic, kJohnsonSecondGenericReplyTopicLines,
 				ARRAYSIZE(kJohnsonSecondGenericReplyTopicLines))) {
-			Common::Error lineError = playJohnsonLine(0xb46);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0xb46, "JOHNSON", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignJohnsonTopicBuffer(0x186);
+			topics.assign(0x186);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x187)) {
 			Common::Error lineError = runtime.playDialogueLineWithVariant(0xb52, "PC", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playJohnsonLine(0xb56, 2);
+			lineError = runtime.playDialogueLineWithVariant(0xb56, "JOHNSON", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignJohnsonTopicBuffer(0x188);
+			topics.assign(0x188);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x189))
 			continue;
 
-		Common::Error lineError = playJohnsonLine(0xb69);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0xb69, "JOHNSON", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		return playJohnsonLine(0xbda, 1);
+		return runtime.playDialogueLineWithVariant(0xbda, "JOHNSON", 1);
 	}
 }
 
diff --git a/engines/harvester/npc/johnson_dialogue.h b/engines/harvester/npc/johnson_dialogue.h
index 53ceed08417..75d9eee8024 100644
--- a/engines/harvester/npc/johnson_dialogue.h
+++ b/engines/harvester/npc/johnson_dialogue.h
@@ -45,7 +45,6 @@ public:
 		int currentTopicBufferLineIndex = -1;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = JohnsonRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.introPending);
diff --git a/engines/harvester/npc/karin_dialogue.cpp b/engines/harvester/npc/karin_dialogue.cpp
index d9d2fae9bf6..a6e2eb74a4d 100644
--- a/engines/harvester/npc/karin_dialogue.cpp
+++ b/engines/harvester/npc/karin_dialogue.cpp
@@ -28,87 +28,60 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kKarinNpc = "KARIN";
-static const char *const kKarinStandingNpc = "KARIN_STANDING";
-static const char *const kKarinCem10Npc = "KARIN_CEM10";
-static const char *const kKarinOfficeNpc = "KARIN_OFFICE";
-static const char *const kPcSpeaker = "PC";
-static const char *const kEdnaNpc = "EDNA";
 
 static const DialogueLineEntry kKarinCem10AliveLines[] = {
-	{ 0x10a5, kPcSpeaker, 4 },
-	{ 0x10a9, kKarinNpc, 3 },
-	{ 0x10ae, kPcSpeaker, 2 },
-	{ 0x10b2, kKarinNpc, 3 },
-	{ 0x10b6, kPcSpeaker, 0 },
-	{ 0x10ba, kKarinNpc, 3 },
-	{ 0x10bf, kPcSpeaker, 0 },
-	{ 0x10c3, kKarinNpc, 3 }
+	{ 0x10a5, "PC", 4 },
+	{ 0x10a9, "KARIN", 3 },
+	{ 0x10ae, "PC", 2 },
+	{ 0x10b2, "KARIN", 3 },
+	{ 0x10b6, "PC", 0 },
+	{ 0x10ba, "KARIN", 3 },
+	{ 0x10bf, "PC", 0 },
+	{ 0x10c3, "KARIN", 3 }
 };
 
 static const DialogueLineEntry kKarinFirstNoItemLines[] = {
-	{ 0x1076, kPcSpeaker, 1 },
-	{ 0x107a, kKarinNpc, 1 },
-	{ 0x107e, kPcSpeaker, 1 },
-	{ 0x1082, kKarinNpc, 1 },
-	{ 0x1087, kPcSpeaker, 0 },
-	{ 0x108b, kKarinNpc, 1 }
+	{ 0x1076, "PC", 1 },
+	{ 0x107a, "KARIN", 1 },
+	{ 0x107e, "PC", 1 },
+	{ 0x1082, "KARIN", 1 },
+	{ 0x1087, "PC", 0 },
+	{ 0x108b, "KARIN", 1 }
 };
 
 static const DialogueLineEntry kKarinFirstPhotoReplyLines[] = {
-	{ 0x10e3, kKarinNpc, 4 },
-	{ 0x10e7, kEdnaNpc, 2 }
+	{ 0x10e3, "KARIN", 4 },
+	{ 0x10e7, "EDNA", 2 }
 };
 
 static const DialogueLineEntry kKarinInvMagLines[] = {
-	{ 0x10ee, kKarinNpc, 0 },
-	{ 0x10f2, kEdnaNpc, 2 }
+	{ 0x10ee, "KARIN", 0 },
+	{ 0x10f2, "EDNA", 2 }
 };
 
 } // End of namespace
 
-bool KarinDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kKarinNpc) ||
-		npcName.equalsIgnoreCase(kKarinStandingNpc) ||
-		npcName.equalsIgnoreCase(kKarinCem10Npc) ||
-		npcName.equalsIgnoreCase(kKarinOfficeNpc);
-}
-
 Common::Error KarinDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	KarinRoomDialogueState &state = _state;
-	auto playKarinLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kKarinNpc, headVariant);
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
-	auto executeActionTagIfSet = [&](const char *tag) {
-		InteractionResult interaction;
-		if (!runtime.executeActionTag(tag, interaction))
-			return;
-
-		runtime.applyImmediateDialogueInteractionEffects(interaction);
-		runtime.queueDialogueInteractionIfNeeded(interaction);
-	};
 
 	const bool karinFoundAlive = runtime.startupScript().getFlagValue("KARIN_FOUND_ALIVE");
 	if (karinFoundAlive && runtime.startupScript().getFlagValue("IN_CEM10") &&
 			!state.cem10AliveLinePlayed) {
 		state.cem10AliveLinePlayed = true;
-		return playSequence(kKarinCem10AliveLines, ARRAYSIZE(kKarinCem10AliveLines));
+		return runtime.playDialogueEntrySequence(kKarinCem10AliveLines, ARRAYSIZE(kKarinCem10AliveLines));
 	}
 
 	if (usedItemName.empty()) {
 		if (!karinFoundAlive && !state.notFoundAliveLinePlayed) {
 			state.notFoundAliveLinePlayed = true;
-			Common::Error lineError = playSequence(
-				kKarinFirstNoItemLines, ARRAYSIZE(kKarinFirstNoItemLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kKarinFirstNoItemLines,
+				ARRAYSIZE(kKarinFirstNoItemLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 
-		return playKarinLine(0x10dc);
+		return runtime.playDialogueLineWithVariant(0x10dc, "KARIN", 0);
 	}
 
 	const bool photoEvidence = usedItemName.equalsIgnoreCase("CASKET_PHOTO") ||
@@ -121,36 +94,36 @@ Common::Error KarinDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			else
 				sharedState.discussedCasketPhotoEvidence = 1;
 
-			Common::Error lineError = playSequence(
-				kKarinFirstPhotoReplyLines, ARRAYSIZE(kKarinFirstPhotoReplyLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kKarinFirstPhotoReplyLines,
+				ARRAYSIZE(kKarinFirstPhotoReplyLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			runtime.setActiveSpeakerPortrait(kKarinNpc, 0);
+			runtime.setActiveSpeakerPortrait("KARIN", 0);
 			state.photoReplyOverrideFlag = true;
 			return Common::kNoError;
 		}
 
-		Common::Error lineError = playKarinLine(0x10e3, 4);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x10e3, "KARIN", 4);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
 		(void)runtime.startupScript().setRuntimeFlagValue("DNALFT_PERVERT", true);
-		executeActionTagIfSet("CALL_POLICE");
+		runtime.executeAndQueueAction("CALL_POLICE");
 		return Common::kNoError;
 	}
 
 	if (usedItemName.equalsIgnoreCase("INV_MAG")) {
-		Common::Error lineError = playSequence(kKarinInvMagLines, ARRAYSIZE(kKarinInvMagLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kKarinInvMagLines, ARRAYSIZE(kKarinInvMagLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
 		(void)runtime.startupScript().setRuntimeFlagValue("DNALFT_PERVERT", true);
-		executeActionTagIfSet("CALL_POLICE");
+		runtime.executeAndQueueAction("CALL_POLICE");
 		return Common::kNoError;
 	}
 
-	return playKarinLine(0x10d6, 1);
+	return runtime.playDialogueLineWithVariant(0x10d6, "KARIN", 1);
 }
 
 } // End of namespace Harvester
diff --git a/engines/harvester/npc/karin_dialogue.h b/engines/harvester/npc/karin_dialogue.h
index 7ef62b48bee..f10dfe27b72 100644
--- a/engines/harvester/npc/karin_dialogue.h
+++ b/engines/harvester/npc/karin_dialogue.h
@@ -34,7 +34,6 @@ public:
 		bool notFoundAliveLinePlayed = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = KarinRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.photoReplyOverrideFlag);
diff --git a/engines/harvester/npc/librarian_dialogue.cpp b/engines/harvester/npc/librarian_dialogue.cpp
index 3de15fb9664..0da747d80bf 100644
--- a/engines/harvester/npc/librarian_dialogue.cpp
+++ b/engines/harvester/npc/librarian_dialogue.cpp
@@ -29,42 +29,36 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kLibrarianNpc = "LIBRARIAN";
-static const char *const kPcSpeaker = "PC";
 static const char *const kRahRoomName = "RAH";
 static const int kLibrarianResponseLineIndex = 0xfa;
 
 static const DialogueLineEntry kLibrarianCainbookTradeLines[] = {
-	{ 0x1db1, kLibrarianNpc, 1 },
-	{ 0x1db7, kPcSpeaker, 0 },
-	{ 0x1dbb, kLibrarianNpc, 0 },
-	{ 0x1dc2, kPcSpeaker, 4 },
-	{ 0x1dc6, kLibrarianNpc, 0 }
+	{ 0x1db1, "LIBRARIAN", 1 },
+	{ 0x1db7, "PC", 0 },
+	{ 0x1dbb, "LIBRARIAN", 0 },
+	{ 0x1dc2, "PC", 4 },
+	{ 0x1dc6, "LIBRARIAN", 0 }
 };
 
 static const DialogueLineEntry kLibrarianIntroLines[] = {
-	{ 0x1d5b, kLibrarianNpc, 0 },
-	{ 0x1d5f, kPcSpeaker, 0 },
-	{ 0x1d63, kLibrarianNpc, 2 },
-	{ 0x1d67, kPcSpeaker, 0 },
-	{ 0x1d6c, kLibrarianNpc, 0 },
-	{ 0x1d77, kLibrarianNpc, 0 }
+	{ 0x1d5b, "LIBRARIAN", 0 },
+	{ 0x1d5f, "PC", 0 },
+	{ 0x1d63, "LIBRARIAN", 2 },
+	{ 0x1d67, "PC", 0 },
+	{ 0x1d6c, "LIBRARIAN", 0 },
+	{ 0x1d77, "LIBRARIAN", 0 }
 };
 
 static const DialogueLineEntry kLibrarianNoItemFollowupLines[] = {
-	{ 0x1d8f, kLibrarianNpc, 0 },
-	{ 0x1d94, kPcSpeaker, 0 },
-	{ 0x1d98, kLibrarianNpc, 0 },
-	{ 0x1da1, kPcSpeaker, 0 },
-	{ 0x1da5, kLibrarianNpc, 0 }
+	{ 0x1d8f, "LIBRARIAN", 0 },
+	{ 0x1d94, "PC", 0 },
+	{ 0x1d98, "LIBRARIAN", 0 },
+	{ 0x1da1, "PC", 0 },
+	{ 0x1da5, "LIBRARIAN", 0 }
 };
 
 } // End of namespace
 
-bool LibrarianDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kLibrarianNpc);
-}
-
 Common::Error LibrarianDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &) {
 	auto hasVisibleInventoryItem = [&](const char *objectName) {
@@ -77,14 +71,12 @@ Common::Error LibrarianDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		return false;
 	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
+
 	auto completeCainbookTrade = [&]() -> Common::Error {
 		(void)runtime.startupScript().addRuntimeObjectToInventory("CLUE");
 
-		Common::Error lineError = playSequence(
-			kLibrarianCainbookTradeLines, ARRAYSIZE(kLibrarianCainbookTradeLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kLibrarianCainbookTradeLines,
+			ARRAYSIZE(kLibrarianCainbookTradeLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -98,12 +90,12 @@ Common::Error LibrarianDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	if (!usedItemName.empty()) {
 		if (!usedItemName.equalsIgnoreCase("CAINBOOK"))
-			return runtime.playDialogueLine(0x1d5b, kLibrarianNpc);
+			return runtime.playDialogueLine(0x1d5b, "LIBRARIAN");
 
 		return completeCainbookTrade();
 	}
 
-	Common::Error lineError = playSequence(kLibrarianIntroLines, ARRAYSIZE(kLibrarianIntroLines));
+	Common::Error lineError = runtime.playDialogueEntrySequence(kLibrarianIntroLines, ARRAYSIZE(kLibrarianIntroLines));
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -113,16 +105,16 @@ Common::Error LibrarianDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return responseError;
 
 	if (responseIndex == 1) {
-		lineError = runtime.playDialogueLine(0x1d85, kLibrarianNpc);
+		lineError = runtime.playDialogueLine(0x1d85, "LIBRARIAN");
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	} else if (responseIndex == 2) {
-		lineError = runtime.playDialogueLineWithVariant(0x1d8b, kLibrarianNpc, 4);
+		lineError = runtime.playDialogueLineWithVariant(0x1d8b, "LIBRARIAN", 4);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
-	return playSequence(kLibrarianNoItemFollowupLines, ARRAYSIZE(kLibrarianNoItemFollowupLines));
+	return runtime.playDialogueEntrySequence(kLibrarianNoItemFollowupLines, ARRAYSIZE(kLibrarianNoItemFollowupLines));
 }
 
 } // End of namespace Harvester
diff --git a/engines/harvester/npc/librarian_dialogue.h b/engines/harvester/npc/librarian_dialogue.h
index fe4a0d4ec14..01eb4b021a9 100644
--- a/engines/harvester/npc/librarian_dialogue.h
+++ b/engines/harvester/npc/librarian_dialogue.h
@@ -28,7 +28,6 @@ namespace Harvester {
 
 class LibrarianDialogueHandler : public NpcDialogueHandler {
 public:
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {}
 	Common::Error handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) override;
diff --git a/engines/harvester/npc/lodge_chef_dialogue.cpp b/engines/harvester/npc/lodge_chef_dialogue.cpp
index b385e4dc225..28ad8b546e5 100644
--- a/engines/harvester/npc/lodge_chef_dialogue.cpp
+++ b/engines/harvester/npc/lodge_chef_dialogue.cpp
@@ -28,28 +28,22 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kLodgeChefNpc = "LODGE_CHEF";
-static const char *const kPcSpeaker = "PC";
 static const int kLodgeChefResponseLine = 0x21;
 
 static const DialogueLineEntry kLodgeChefIntroLines[] = {
-	{ 0x1b10, kLodgeChefNpc, 2 },
-	{ 0x1b19, kLodgeChefNpc, 0 },
-	{ 0x1b1a, kLodgeChefNpc, 3 },
-	{ 0x1b1b, kLodgeChefNpc, 0 }
+	{ 0x1b10, "LODGE_CHEF", 2 },
+	{ 0x1b19, "LODGE_CHEF", 0 },
+	{ 0x1b1a, "LODGE_CHEF", 3 },
+	{ 0x1b1b, "LODGE_CHEF", 0 }
 };
 
 static const DialogueLineEntry kLodgeChefTailLines[] = {
-	{ 0x1b36, kPcSpeaker, 0 },
-	{ 0x1b3a, kLodgeChefNpc, 3 }
+	{ 0x1b36, "PC", 0 },
+	{ 0x1b3a, "LODGE_CHEF", 3 }
 };
 
 } // End of namespace
 
-bool LodgeChefDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kLodgeChefNpc);
-}
-
 Common::Error LodgeChefDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
 	Common::Error lineError = runtime.playDialogueEntrySequence(
@@ -64,9 +58,9 @@ Common::Error LodgeChefDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return responseError;
 
 	if (responseIndex == 1) {
-		lineError = runtime.playDialogueLineWithVariant(0x1b2c, kLodgeChefNpc, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x1b2c, "LODGE_CHEF", 3);
 	} else if (responseIndex == 2) {
-		lineError = runtime.playDialogueLineWithVariant(0x1b31, kLodgeChefNpc, 0);
+		lineError = runtime.playDialogueLineWithVariant(0x1b31, "LODGE_CHEF", 0);
 	} else {
 		lineError = Common::kNoError;
 	}
diff --git a/engines/harvester/npc/lodge_chef_dialogue.h b/engines/harvester/npc/lodge_chef_dialogue.h
index b0d3a563571..eaeb1793358 100644
--- a/engines/harvester/npc/lodge_chef_dialogue.h
+++ b/engines/harvester/npc/lodge_chef_dialogue.h
@@ -28,7 +28,6 @@ namespace Harvester {
 
 class LodgeChefDialogueHandler : public NpcDialogueHandler {
 public:
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {}
 	Common::Error handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) override;
diff --git a/engines/harvester/npc/loomis_dialogue.cpp b/engines/harvester/npc/loomis_dialogue.cpp
index 726d74c312c..101f8dbf22f 100644
--- a/engines/harvester/npc/loomis_dialogue.cpp
+++ b/engines/harvester/npc/loomis_dialogue.cpp
@@ -30,8 +30,6 @@ namespace Harvester {
 namespace {
 
 static const int kLoomisSheriffTopicLineIndices[] = { 0x5f, 0x60, 0x61 };
-static const char *const kLoomisNpc = "LOOMIS";
-static const char *const kPcSpeaker = "PC";
 static const char *const kLoomisTopicAlias = "LOOMIS";
 static const char *const kPhelpsTopicAlias = "Phelps";
 static const char *const kMensNeedsTopicAlias = "Man's Needs";
@@ -39,61 +37,42 @@ static const char *const kPcFryDaddyActionTag = "PC_FRY_DADDY_1";
 static const char *const kDialogueC048FstPath = "GRAPHIC/FST/C048.FST";
 
 static const DialogueLineEntry kLoomisIntroOpeningLines[] = {
-	{ 0x10fd, kLoomisNpc, 0 },
-	{ 0x1101, kPcSpeaker, 0 },
-	{ 0x1105, kLoomisNpc, 0 },
-	{ 0x110a, kPcSpeaker, 0 },
-	{ 0x110e, kLoomisNpc, 0 }
+	{ 0x10fd, "LOOMIS", 0 },
+	{ 0x1101, "PC", 0 },
+	{ 0x1105, "LOOMIS", 0 },
+	{ 0x110a, "PC", 0 },
+	{ 0x110e, "LOOMIS", 0 }
 };
 
 static const DialogueLineEntry kLoomisReturnVisitFollowupLines[] = {
-	{ 0x11a8, kLoomisNpc, 1 },
-	{ 0x11ac, kPcSpeaker, 0 },
-	{ 0x11b0, kLoomisNpc, 3 }
+	{ 0x11a8, "LOOMIS", 1 },
+	{ 0x11ac, "PC", 0 },
+	{ 0x11b0, "LOOMIS", 3 }
 };
 
 static const DialogueLineEntry kLoomisMrsLoomisLines[] = {
-	{ 0x114f, kLoomisNpc, 1 },
-	{ 0x1150, kLoomisNpc, 2 },
-	{ 0x1151, kLoomisNpc, 2 },
-	{ 0x1152, kLoomisNpc, 2 },
-	{ 0x1153, kLoomisNpc, 3 }
+	{ 0x114f, "LOOMIS", 1 },
+	{ 0x1150, "LOOMIS", 2 },
+	{ 0x1151, "LOOMIS", 2 },
+	{ 0x1152, "LOOMIS", 2 },
+	{ 0x1153, "LOOMIS", 3 }
 };
 
 static const DialogueLineEntry kLoomisMrsPhelpsLines[] = {
-	{ 0x1161, kLoomisNpc, 0 },
-	{ 0x1162, kLoomisNpc, 3 },
-	{ 0x1163, kLoomisNpc, 3 }
+	{ 0x1161, "LOOMIS", 0 },
+	{ 0x1162, "LOOMIS", 3 },
+	{ 0x1163, "LOOMIS", 3 }
 };
 
 } // End of namespace
 
-bool LoomisDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase("LOOMIS");
-}
-
 Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	LoomisRoomDialogueState &state = _state;
 	Common::String &loomisTopicBuffer = state.currentTopicBuffer;
 	int &loomisTopicBufferLineIndex = state.currentTopicBufferLineIndex;
 
-	auto assignLoomisTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(loomisTopicBuffer, loomisTopicBufferLineIndex,
-			responseLineIndex, "Loomis topic buffer");
-	};
-	auto playLoomisLine = [&](int wavId, const char *speakerId = kLoomisNpc,
-			int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, speakerId, headVariant);
-	};
-	auto executeLoomisActionTag = [&](const char *tag) {
-		InteractionResult interaction;
-		if (!runtime.executeActionTag(tag, interaction))
-			return;
-
-		runtime.applyImmediateDialogueInteractionEffects(interaction);
-		runtime.queueDialogueInteractionIfNeeded(interaction);
-	};
+	DialogueTopicBuffer topics(runtime, loomisTopicBuffer, loomisTopicBufferLineIndex, "Loomis topic buffer");
 
 	if (usedItemName.empty()) {
 		Common::Error lineError = Common::kNoError;
@@ -109,16 +88,16 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 				sharedState.dialogueStateD2f04 = true;
 				if (runtime.startupScript().getFlagValue("SHERIFF_IN_DINER")) {
-					lineError = playLoomisLine(0x1112, kPcSpeaker);
+					lineError = runtime.playDialogueLineWithVariant(0x1112, "PC", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
-					lineError = playLoomisLine(0x1118);
+					lineError = runtime.playDialogueLineWithVariant(0x1118, "LOOMIS", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				}
 			}
 
-			lineError = playLoomisLine(0x1121);
+			lineError = runtime.playDialogueLineWithVariant(0x1121, "LOOMIS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -127,7 +106,7 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 
-			lineError = playLoomisLine(0x1130, kLoomisNpc, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x1130, "LOOMIS", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else if (state.returnVisitFollowupPending) {
@@ -137,7 +116,7 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else {
-			lineError = playLoomisLine(0x11f6);
+			lineError = runtime.playDialogueLineWithVariant(0x11f6, "LOOMIS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -145,19 +124,19 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED") &&
 				!state.stephMidgameShown) {
 			state.stephMidgameShown = true;
-			lineError = playLoomisLine(0x1230, kLoomisNpc, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x1230, "LOOMIS", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 		if (runtime.startupScript().getCurrentStoryDayIndex() == 5 &&
 				!state.dayFiveShown) {
 			state.dayFiveShown = true;
-			lineError = playLoomisLine(0x123c);
+			lineError = runtime.playDialogueLineWithVariant(0x123c, "LOOMIS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 
-		assignLoomisTopicBuffer(0x54);
+		topics.assign(0x54);
 		for (;;) {
 			Common::String selectedTopic;
 			Common::Error menuError = runtime.runKeywordMenu(
@@ -169,14 +148,14 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 			if (selectedTopic.equalsIgnoreCase(runtime.genericByeTopic()) ||
 					runtime.matchesResponseLine(selectedTopic, 0x56)) {
-				return playLoomisLine(0x11a1, kLoomisNpc, 1);
+				return runtime.playDialogueLineWithVariant(0x11a1, "LOOMIS", 1);
 			}
 
 			if (runtime.matchesResponseLine(selectedTopic, 0x57)) {
-				lineError = playLoomisLine(0x1143, kLoomisNpc, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x1143, "LOOMIS", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignLoomisTopicBuffer(0x58);
+				topics.assign(0x58);
 				continue;
 			}
 			if (runtime.matchesResponseLine(selectedTopic, 0x59) ||
@@ -185,7 +164,7 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 					kLoomisMrsLoomisLines, ARRAYSIZE(kLoomisMrsLoomisLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignLoomisTopicBuffer(0x5a);
+				topics.assign(0x5a);
 				continue;
 			}
 			if (runtime.matchesResponseLine(selectedTopic, 0x5b) ||
@@ -194,34 +173,34 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 					kLoomisMrsPhelpsLines, ARRAYSIZE(kLoomisMrsPhelpsLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignLoomisTopicBuffer(0x5c);
+				topics.assign(0x5c);
 				continue;
 			}
 			if (runtime.matchesResponseLine(selectedTopic, 0x5d) ||
 					selectedTopic.equalsIgnoreCase(kMensNeedsTopicAlias)) {
-				lineError = playLoomisLine(0x1170, kLoomisNpc, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x1170, "LOOMIS", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignLoomisTopicBuffer(0x5e);
+				topics.assign(0x5e);
 				continue;
 			}
 			if (runtime.matchesAnyResponseLine(selectedTopic,
 					kLoomisSheriffTopicLineIndices, ARRAYSIZE(kLoomisSheriffTopicLineIndices))) {
-				lineError = playLoomisLine(0x117e);
+				lineError = runtime.playDialogueLineWithVariant(0x117e, "LOOMIS", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignLoomisTopicBuffer(0x62);
+				topics.assign(0x62);
 				continue;
 			}
 			if (runtime.matchesResponseLine(selectedTopic, 0x63)) {
-				lineError = playLoomisLine(0x118a);
+				lineError = runtime.playDialogueLineWithVariant(0x118a, "LOOMIS", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignLoomisTopicBuffer(0x64);
+				topics.assign(0x64);
 				continue;
 			}
 			if (runtime.matchesResponseLine(selectedTopic, 0x65)) {
-				lineError = playLoomisLine(0x1196, kLoomisNpc, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x1196, "LOOMIS", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				continue;
@@ -229,7 +208,7 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (runtime.matchesResponseLine(selectedTopic, 0x66))
 				continue;
 
-			lineError = playLoomisLine(0x11fc);
+			lineError = runtime.playDialogueLineWithVariant(0x11fc, "LOOMIS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -243,7 +222,7 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			usedItemName.equalsIgnoreCase("TV_DEED_PHOTOCOPY")) {
 		(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownEvidenceOfBlackmail, true);
 		sharedState.discussedNoteCheckbookEvidence = 1;
-		Common::Error lineError = playLoomisLine(0x1209);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x1209, "LOOMIS", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -253,20 +232,20 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playLoomisLine(0x1214);
+			lineError = runtime.playDialogueLineWithVariant(0x1214, "LOOMIS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else if (responseIndex == 2) {
-			lineError = playLoomisLine(0x1219);
+			lineError = runtime.playDialogueLineWithVariant(0x1219, "LOOMIS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 
-		lineError = playLoomisLine(0x121f, kLoomisNpc, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x121f, "LOOMIS", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		executeLoomisActionTag(kPcFryDaddyActionTag);
+		runtime.executeAndQueueAction(kPcFryDaddyActionTag);
 		return Common::kNoError;
 	}
 	if ((usedItemName.equalsIgnoreCase("LEDGER") ||
@@ -274,15 +253,15 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			runtime.startupScript().getFlagValue("HAVE_BOTH_LEDGERS")) {
 		(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownLedgersToAnyone, true);
 		sharedState.discussedLedgerEvidence = 1;
-		return playLoomisLine(0x1229);
+		return runtime.playDialogueLineWithVariant(0x1229, "LOOMIS", 0);
 	}
 	if (usedItemName.equalsIgnoreCase("INV_MAG")) {
 		if (!runtime.startupScript().getFlagValue("SHERIFF_IN_DINER")) {
 			(void)runtime.startupScript().addRuntimeObjectToInventory("INV_MAG");
-			return playLoomisLine(0x11db, kLoomisNpc, 1);
+			return runtime.playDialogueLineWithVariant(0x11db, "LOOMIS", 1);
 		}
 
-		Common::Error lineError = playLoomisLine(0x11bb);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x11bb, "LOOMIS", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -292,7 +271,7 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playLoomisLine(0x11c6, kLoomisNpc, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x11c6, "LOOMIS", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -316,13 +295,13 @@ Common::Error LoomisDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		(void)runtime.startupScript().addRuntimeObjectToInventory("INV_MAG");
 		if (responseIndex == 2)
-			return playLoomisLine(0x11cc, kLoomisNpc, 3);
+			return runtime.playDialogueLineWithVariant(0x11cc, "LOOMIS", 3);
 		return Common::kNoError;
 	}
 	if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL"))
-		return playLoomisLine(0x1143, kLoomisNpc, 1);
+		return runtime.playDialogueLineWithVariant(0x1143, "LOOMIS", 1);
 
-	return playLoomisLine(0x1202);
+	return runtime.playDialogueLineWithVariant(0x1202, "LOOMIS", 0);
 }
 
 } // End of namespace Harvester
diff --git a/engines/harvester/npc/loomis_dialogue.h b/engines/harvester/npc/loomis_dialogue.h
index a340151260a..1a7313873e7 100644
--- a/engines/harvester/npc/loomis_dialogue.h
+++ b/engines/harvester/npc/loomis_dialogue.h
@@ -37,7 +37,6 @@ public:
 		bool dayFiveShown = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = LoomisRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueString(s, _state.currentTopicBuffer);
diff --git a/engines/harvester/npc/madam_dialogue.cpp b/engines/harvester/npc/madam_dialogue.cpp
index cc6870879b0..cee280493f4 100644
--- a/engines/harvester/npc/madam_dialogue.cpp
+++ b/engines/harvester/npc/madam_dialogue.cpp
@@ -28,8 +28,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kMadamNpc = "MADAM";
-static const char *const kPcSpeaker = "PC";
 static const char *const kHookerNpc = "HOOKER";
 static const char *const kBarCash200ItemName = "BARCASH200";
 static const char *const kInventoryOwnerName = "INVENTORY";
@@ -43,18 +41,8 @@ static const int kMadamPaymentResponseLine = 0xfc;
 
 } // End of namespace
 
-bool MadamDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kMadamNpc);
-}
-
 Common::Error MadamDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
-	auto playMadamLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kMadamNpc, headVariant);
-	};
-	auto playPcLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kPcSpeaker, headVariant);
-	};
 	auto queueHookerMonsterfyTransition = [&]() {
 		InteractionResult interaction;
 		if (runtime.startupScript().queueRuntimeNpcDeathOrMonsterfy(kHookerNpc)) {
@@ -79,7 +67,7 @@ Common::Error MadamDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	(void)runtime.startupScript().setRuntimeFlagValue(kMadamInterruptFlag, false);
 
-	Common::Error lineError = playMadamLine(0x1537, 1);
+	Common::Error lineError = runtime.playDialogueLineWithVariant(0x1537, "MADAM", 1);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -89,11 +77,11 @@ Common::Error MadamDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return responseError;
 
 	if (responseIndex != 1) {
-		lineError = playMadamLine(0x1577);
+		lineError = runtime.playDialogueLineWithVariant(0x1577, "MADAM", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playMadamLine(0x157b, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x157b, "MADAM", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -101,20 +89,20 @@ Common::Error MadamDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return Common::kNoError;
 	}
 
-	lineError = playMadamLine(0x1542, 1);
+	lineError = runtime.playDialogueLineWithVariant(0x1542, "MADAM", 1);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
 	if (!runtime.startupScript().isObjectInInventory(kBarCash200ItemName)) {
-		lineError = playPcLine(0x156e);
+		lineError = runtime.playDialogueLineWithVariant(0x156e, "PC", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playMadamLine(0x1572);
+		lineError = runtime.playDialogueLineWithVariant(0x1572, "MADAM", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playMadamLine(0x157b, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x157b, "MADAM", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -128,11 +116,11 @@ Common::Error MadamDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return responseError;
 
 	if (responseIndex == 1) {
-		lineError = playMadamLine(0x154e);
+		lineError = runtime.playDialogueLineWithVariant(0x154e, "MADAM", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playMadamLine(0x157b, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x157b, "MADAM", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -142,7 +130,7 @@ Common::Error MadamDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (responseIndex != 2)
 		return Common::kNoError;
 
-	lineError = playMadamLine(0x1555, 1);
+	lineError = runtime.playDialogueLineWithVariant(0x1555, "MADAM", 1);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -153,11 +141,11 @@ Common::Error MadamDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playMadamLine(0x155a);
+	lineError = runtime.playDialogueLineWithVariant(0x155a, "MADAM", 0);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playMadamLine(0x1563);
+	lineError = runtime.playDialogueLineWithVariant(0x1563, "MADAM", 0);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
diff --git a/engines/harvester/npc/madam_dialogue.h b/engines/harvester/npc/madam_dialogue.h
index 3dc37f955c8..efd54b10f0e 100644
--- a/engines/harvester/npc/madam_dialogue.h
+++ b/engines/harvester/npc/madam_dialogue.h
@@ -28,7 +28,6 @@ namespace Harvester {
 
 class MadamDialogueHandler : public NpcDialogueHandler {
 public:
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {}
 	Common::Error handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) override;
diff --git a/engines/harvester/npc/maint_man_dialogue.cpp b/engines/harvester/npc/maint_man_dialogue.cpp
index d95720e643c..36b871f295d 100644
--- a/engines/harvester/npc/maint_man_dialogue.cpp
+++ b/engines/harvester/npc/maint_man_dialogue.cpp
@@ -28,17 +28,12 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kMaintManNpc = "MAINT_MAN";
 static const char *const kMaintManFirstConversationFlag = "MAINTENANCE_MAN_FIRST_CONVERSATION";
 static const char *const kMaintManThirdConversationFlag = "MAINTENANCE_MAN_THIRD_CONVERSATION";
 static const char *const kMaintManFourthConversationFlag = "MAINTENANCE_MAN_FOURTH_CONVERSATION";
 
 } // End of namespace
 
-bool MaintManDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kMaintManNpc);
-}
-
 Common::Error MaintManDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
 	int wavId = 0xdb6;
@@ -48,15 +43,15 @@ Common::Error MaintManDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		wavId = 0xdaf;
 		headVariant = 1;
 	} else if (runtime.startupScript().getFlagValue(kMaintManThirdConversationFlag)) {
-		return runtime.playDialogueLineWithVariant(0xdbc, kMaintManNpc, 2);
+		return runtime.playDialogueLineWithVariant(0xdbc, "MAINT_MAN", 2);
 	} else if (runtime.startupScript().getFlagValue(kMaintManFourthConversationFlag)) {
-		return runtime.playDialogueLineWithVariant(0xdc4, kMaintManNpc, 2);
+		return runtime.playDialogueLineWithVariant(0xdc4, "MAINT_MAN", 2);
 	}
 
 	if (headVariant != 0)
-		return runtime.playDialogueLineWithVariant(wavId, kMaintManNpc, headVariant);
+		return runtime.playDialogueLineWithVariant(wavId, "MAINT_MAN", headVariant);
 
-	return runtime.playDialogueLine(wavId, kMaintManNpc);
+	return runtime.playDialogueLine(wavId, "MAINT_MAN");
 }
 
 } // End of namespace Harvester
diff --git a/engines/harvester/npc/maint_man_dialogue.h b/engines/harvester/npc/maint_man_dialogue.h
index 0b08e377c7d..137fbcc24f5 100644
--- a/engines/harvester/npc/maint_man_dialogue.h
+++ b/engines/harvester/npc/maint_man_dialogue.h
@@ -28,7 +28,6 @@ namespace Harvester {
 
 class MaintManDialogueHandler : public NpcDialogueHandler {
 public:
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {}
 	Common::Error handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) override;
diff --git a/engines/harvester/npc/mcknight_dialogue.cpp b/engines/harvester/npc/mcknight_dialogue.cpp
index 8bebe53b3c7..8afd3e0eb01 100644
--- a/engines/harvester/npc/mcknight_dialogue.cpp
+++ b/engines/harvester/npc/mcknight_dialogue.cpp
@@ -29,55 +29,34 @@
 
 namespace Harvester {
 
-namespace {
-
-static const char *const kMcknightNpc = "MCKNIGHT";
-static const char *const kPcSpeaker = "PC";
-
-} // End of namespace
-
-bool McknightDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kMcknightNpc);
-}
-
 Common::Error McknightDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
-	auto playMcknightLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kMcknightNpc, headVariant);
-	};
-	auto playPcLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kPcSpeaker, headVariant);
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
-
 	if (runtime.startupScript().getFlagValue("LOOK_SAFE_2ND"))
-		return playMcknightLine(0x15d0, 2);
+		return runtime.playDialogueLineWithVariant(0x15d0, "MCKNIGHT", 2);
 
 	if (runtime.startupScript().getFlagValue("LOOK_SAFE_1ST") && !_state.lookSafeFirstLineGate) {
 		_state.lookSafeFirstLineGate = true;
-		return playMcknightLine(0x15c9, 2);
+		return runtime.playDialogueLineWithVariant(0x15c9, "MCKNIGHT", 2);
 	}
 
 	if (usedItemName.empty()) {
 		if (_state.talkStatePending) {
 			_state.talkStatePending = false;
 			const DialogueLineEntry lines[] = {
-				{ 0x1587, kMcknightNpc, 2 },
-				{ 0x158b, kPcSpeaker, 0 },
-				{ 0x158f, kMcknightNpc, 0 },
-				{ 0x1594, kPcSpeaker, 0 },
-				{ 0x1598, kMcknightNpc, 2 }
+				{ 0x1587, "MCKNIGHT", 2 },
+				{ 0x158b, "PC", 0 },
+				{ 0x158f, "MCKNIGHT", 0 },
+				{ 0x1594, "PC", 0 },
+				{ 0x1598, "MCKNIGHT", 2 }
 			};
-			Common::Error lineError = playSequence(lines, ARRAYSIZE(lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			sharedState.dialogueStateD2f04 = true;
-			lineError = playPcLine(0x159d);
+			lineError = runtime.playDialogueLineWithVariant(0x159d, "PC", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playMcknightLine(0x15a2, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x15a2, "MCKNIGHT", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			int responseIndex = 0;
@@ -85,50 +64,50 @@ Common::Error McknightDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (responseError.getCode() != Common::kNoError)
 				return responseError;
 			if (responseIndex == 1)
-				return playMcknightLine(0x15af);
+				return runtime.playDialogueLineWithVariant(0x15af, "MCKNIGHT", 0);
 			if (responseIndex == 2) {
 				const DialogueLineEntry replyLines[] = {
-					{ 0x15b5, kMcknightNpc, 4 },
-					{ 0x15bd, kPcSpeaker, 0 },
-					{ 0x15c1, kMcknightNpc, 2 }
+					{ 0x15b5, "MCKNIGHT", 4 },
+					{ 0x15bd, "PC", 0 },
+					{ 0x15c1, "MCKNIGHT", 2 }
 				};
-				return playSequence(replyLines, ARRAYSIZE(replyLines));
+				return runtime.playDialogueEntrySequence(replyLines, ARRAYSIZE(replyLines));
 			}
 		}
-		return playMcknightLine(0x15e4);
+		return runtime.playDialogueLineWithVariant(0x15e4, "MCKNIGHT", 0);
 	}
 
 	if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
 		sharedState.discussedWhaleyHerrillPhoto = 1;
 		(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownPhotoOfWhaleyHerrill, true);
-		return playMcknightLine(0x15ea, 1);
+		return runtime.playDialogueLineWithVariant(0x15ea, "MCKNIGHT", 1);
 	}
 	if (usedItemName.equalsIgnoreCase("CASKET_PHOTO") ||
 			usedItemName.equalsIgnoreCase("CASKET_PHOTOCOPY")) {
 		sharedState.discussedCasketPhotoEvidence = 1;
 		(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownPhotoOfCorpse, true);
-		Common::Error lineError = playMcknightLine(0x15f2);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x15f2, "MCKNIGHT", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
 		if (sharedState.dialogueStateD2ebc) {
-			lineError = playPcLine(0x15f8);
+			lineError = runtime.playDialogueLineWithVariant(0x15f8, "PC", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			return playMcknightLine(0x15fd);
+			return runtime.playDialogueLineWithVariant(0x15fd, "MCKNIGHT", 0);
 		}
 
-		lineError = playPcLine(0x1605);
+		lineError = runtime.playDialogueLineWithVariant(0x1605, "PC", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		return playMcknightLine(0x1609);
+		return runtime.playDialogueLineWithVariant(0x1609, "MCKNIGHT", 0);
 	}
 	if ((usedItemName.equalsIgnoreCase("LEDGER") ||
 				usedItemName.equalsIgnoreCase("LEDGER2")) &&
 			runtime.startupScript().getFlagValue("HAVE_BOTH_LEDGERS")) {
 		sharedState.discussedLedgerEvidence = 1;
 		(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownLedgersToAnyone, true);
-		return playMcknightLine(0x1611, 1);
+		return runtime.playDialogueLineWithVariant(0x1611, "MCKNIGHT", 1);
 	}
 	if (usedItemName.equalsIgnoreCase("NOTE") ||
 			usedItemName.equalsIgnoreCase("NOTE_PHOTOCOPY") ||
@@ -137,16 +116,16 @@ Common::Error McknightDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		sharedState.discussedNoteCheckbookEvidence = 1;
 		(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownEvidenceOfBlackmail, true);
 		const DialogueLineEntry lines[] = {
-			{ 0x161b, kMcknightNpc, 1 },
-			{ 0x1621, kPcSpeaker, 0 },
-			{ 0x1626, kMcknightNpc, 3 },
-			{ 0x162b, kPcSpeaker, 4 },
-			{ 0x1631, kMcknightNpc, 0 }
+			{ 0x161b, "MCKNIGHT", 1 },
+			{ 0x1621, "PC", 0 },
+			{ 0x1626, "MCKNIGHT", 3 },
+			{ 0x162b, "PC", 4 },
+			{ 0x1631, "MCKNIGHT", 0 }
 		};
-		return playSequence(lines, ARRAYSIZE(lines));
+		return runtime.playDialogueEntrySequence(lines, ARRAYSIZE(lines));
 	}
 
-	return playMcknightLine(0x15de);
+	return runtime.playDialogueLineWithVariant(0x15de, "MCKNIGHT", 0);
 }
 
 } // End of namespace Harvester
diff --git a/engines/harvester/npc/mcknight_dialogue.h b/engines/harvester/npc/mcknight_dialogue.h
index df202a9d3ff..87cd2653757 100644
--- a/engines/harvester/npc/mcknight_dialogue.h
+++ b/engines/harvester/npc/mcknight_dialogue.h
@@ -33,7 +33,6 @@ public:
 		bool lookSafeFirstLineGate = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = McknightRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.talkStatePending);
diff --git a/engines/harvester/npc/memb_dir_dialogue.cpp b/engines/harvester/npc/memb_dir_dialogue.cpp
index 83db47e0b09..cfe9d8e2ad0 100644
--- a/engines/harvester/npc/memb_dir_dialogue.cpp
+++ b/engines/harvester/npc/memb_dir_dialogue.cpp
@@ -28,23 +28,14 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kMembDirNpc = "MEMB_DIR";
 static const int kMembDirResponseLineIndex = 0xfe;
 
 } // End of namespace
 
-bool MembDirDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kMembDirNpc);
-}
-
 Common::Error MembDirDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
-	auto playMembDirLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kMembDirNpc, headVariant);
-	};
-
 	if (!runtime.startupScript().getFlagValue("PC_KILLED_KEWPIE")) {
-		Common::Error lineError = playMembDirLine(0x2b7, 1);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x2b7, "MEMB_DIR", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -54,9 +45,9 @@ Common::Error MembDirDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1)
-			return playMembDirLine(0x2c2);
+			return runtime.playDialogueLineWithVariant(0x2c2, "MEMB_DIR", 0);
 		if (responseIndex == 2)
-			return playMembDirLine(0x2cb);
+			return runtime.playDialogueLineWithVariant(0x2cb, "MEMB_DIR", 0);
 
 		return Common::kNoError;
 	}
@@ -65,14 +56,14 @@ Common::Error MembDirDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return Common::kNoError;
 
 	_state.kewpieKillLinePlayed = true;
-	Common::Error lineError = playMembDirLine(0x2d5, 1);
+	Common::Error lineError = runtime.playDialogueLineWithVariant(0x2d5, "MEMB_DIR", 1);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
 	if (runtime.startupScript().getFlagValue("TOOK_THE_DOLL"))
-		return playMembDirLine(0x2e3);
+		return runtime.playDialogueLineWithVariant(0x2e3, "MEMB_DIR", 0);
 
-	return playMembDirLine(0x2ea);
+	return runtime.playDialogueLineWithVariant(0x2ea, "MEMB_DIR", 0);
 }
 
 } // End of namespace Harvester
diff --git a/engines/harvester/npc/memb_dir_dialogue.h b/engines/harvester/npc/memb_dir_dialogue.h
index 4ca07624980..7cbaaf7fbf3 100644
--- a/engines/harvester/npc/memb_dir_dialogue.h
+++ b/engines/harvester/npc/memb_dir_dialogue.h
@@ -32,7 +32,6 @@ public:
 		bool kewpieKillLinePlayed = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = MembDirRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.kewpieKillLinePlayed);
diff --git a/engines/harvester/npc/mom_dialogue.cpp b/engines/harvester/npc/mom_dialogue.cpp
index 2bd2e8fa0aa..be474d65cae 100644
--- a/engines/harvester/npc/mom_dialogue.cpp
+++ b/engines/harvester/npc/mom_dialogue.cpp
@@ -474,36 +474,14 @@ static const int kMomGoodCauseDay5FollowupTopicBufferResponseLine = 0x15c;
 
 } // End of namespace
 
-bool MomDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase("MOM");
-}
-
 Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	const int currentStoryDayIndex = runtime.startupScript().getCurrentStoryDayIndex();
 	MomRoomDialogueState &state = _state;
 	Common::String momTopicBuffer;
 	int momTopicBufferLineIndex = -1;
-	auto assignMomTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(momTopicBuffer, momTopicBufferLineIndex,
-			responseLineIndex, "Mom topic buffer");
-	};
-	auto playMomLine = [&](int wavId, int headVariant) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, "MOM", headVariant);
-	};
-	auto playPcLine = [&](int wavId, int headVariant) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, "PC", headVariant);
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
-	auto executeActionTagIfSet = [&](const char *actionTag) {
-		InteractionResult interaction;
-		if (runtime.executeActionTag(actionTag, interaction)) {
-			runtime.applyImmediateDialogueInteractionEffects(interaction);
-			runtime.queueDialogueInteractionIfNeeded(interaction);
-		}
-	};
+	DialogueTopicBuffer topics(runtime, momTopicBuffer, momTopicBufferLineIndex, "Mom topic buffer");
+
 	auto hasInventoryObject = [&](const char *objectName) -> bool {
 		Common::Array<ObjectRecord> inventoryObjects;
 		runtime.startupScript().getVisibleInventoryObjects(inventoryObjects);
@@ -515,11 +493,11 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	};
 	auto runMomGoodbye = [&]() -> Common::Error {
 		if (!runtime.startupScript().getFlagValue("DAY_FLAG"))
-			return playMomLine(0x256e, 1);
+			return runtime.playDialogueLineWithVariant(0x256e, "MOM", 1);
 		if (!runtime.startupScript().isNamedNpcDeathTypeClear("JIMMY"))
 			return Common::kNoError;
 
-		return playMomLine(0x2051, 1);
+		return runtime.playDialogueLineWithVariant(0x2051, "MOM", 1);
 	};
 
 	if (!usedItemName.empty()) {
@@ -529,34 +507,34 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				usedItemName.equalsIgnoreCase("CHECKBOOK_PHOTOCOPY")) {
 			sharedState.discussedNoteCheckbookEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownEvidenceOfBlackmail, true);
-			return playMomLine(0x2317, 1);
+			return runtime.playDialogueLineWithVariant(0x2317, "MOM", 1);
 		}
 		if ((usedItemName.equalsIgnoreCase("LEDGER") ||
 				usedItemName.equalsIgnoreCase("LEDGER2")) &&
 				runtime.startupScript().getFlagValue("HAVE_BOTH_LEDGERS")) {
 			sharedState.discussedLedgerEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownLedgersToAnyone, true);
-			return playSequence(kMomLedgerLines, ARRAYSIZE(kMomLedgerLines));
+			return runtime.playDialogueEntrySequence(kMomLedgerLines, ARRAYSIZE(kMomLedgerLines));
 		}
 		if (usedItemName.equalsIgnoreCase("CASKET_PHOTO") ||
 				usedItemName.equalsIgnoreCase("CASKET_PHOTOCOPY")) {
 			sharedState.discussedCasketPhotoEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownPhotoOfCorpse, true);
-			return playSequence(kMomCasketPhotoLines, ARRAYSIZE(kMomCasketPhotoLines));
+			return runtime.playDialogueEntrySequence(kMomCasketPhotoLines, ARRAYSIZE(kMomCasketPhotoLines));
 		}
 		if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
 			sharedState.discussedWhaleyHerrillPhoto = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownPhotoOfWhaleyHerrill, true);
-			return playSequence(kMomWhaleyPhotoLines, ARRAYSIZE(kMomWhaleyPhotoLines));
+			return runtime.playDialogueEntrySequence(kMomWhaleyPhotoLines, ARRAYSIZE(kMomWhaleyPhotoLines));
 		}
 		if (usedItemName.equalsIgnoreCase("TV_DEED") ||
 				usedItemName.equalsIgnoreCase("TV_DEED_PHOTOCOPY")) {
 			sharedState.discussedTvDeedEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownEvidenceSheriffOwns, true);
-			return playMomLine(0x2382, 1);
+			return runtime.playDialogueLineWithVariant(0x2382, "MOM", 1);
 		}
 
-		return playMomLine(0x26cc, 2);
+		return runtime.playDialogueLineWithVariant(0x26cc, "MOM", 2);
 	}
 
 	if (state.introPending) {
@@ -566,17 +544,17 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		state.introDayIndex = currentStoryDayIndex;
 		sharedState.dialogueStateD2ef4 = true;
 
-		Common::Error lineError = playSequence(kMomIntroLines, ARRAYSIZE(kMomIntroLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMomIntroLines, ARRAYSIZE(kMomIntroLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		assignMomTopicBuffer(kMomInitialTopicBufferResponseLine);
+		topics.assign(kMomInitialTopicBufferResponseLine);
 	} else if (state.sameDayIntroLineEnabled && currentStoryDayIndex == state.introDayIndex) {
-		Common::Error lineError = playMomLine(0x2047, 0);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x2047, "MOM", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		assignMomTopicBuffer(kMomSameDayTopicBufferResponseLine);
+		topics.assign(kMomSameDayTopicBufferResponseLine);
 	} else if (state.postIntroDefaultLineEnabled) {
-		Common::Error lineError = playMomLine(0x2311, 1);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x2311, "MOM", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -585,18 +563,17 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.stephMidgameShown) {
 		state.stephMidgameShown = true;
 
-		Common::Error lineError = playSequence(
-			kMomStephMidgameOpeningLines, ARRAYSIZE(kMomStephMidgameOpeningLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMomStephMidgameOpeningLines,
+			ARRAYSIZE(kMomStephMidgameOpeningLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		lineError = runtime.playDialogueFst(kDialogueC009FstPath);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playSequence(
-			kMomStephMidgameClosingLines, ARRAYSIZE(kMomStephMidgameClosingLines));
+		lineError = runtime.playDialogueEntrySequence(kMomStephMidgameClosingLines, ARRAYSIZE(kMomStephMidgameClosingLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		assignMomTopicBuffer(kMomStephMidgameTopicBufferResponseLine);
+		topics.assign(kMomStephMidgameTopicBufferResponseLine);
 	}
 
 	if (runtime.startupScript().getFlagValue("DINER_BURNED") &&
@@ -605,11 +582,11 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.dinerBurnedKarinMissingOrDeadShown) {
 		state.dinerBurnedKarinMissingOrDeadShown = true;
 
-		Common::Error lineError = playMomLine(0x2456, 0);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x2456, "MOM", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (sharedState.dialogueStateD2ea4) {
-			lineError = playMomLine(0x245d, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x245d, "MOM", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -620,10 +597,10 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 			switch (responseIndex) {
 			case 1:
-				lineError = playMomLine(0x246b, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x246b, "MOM", 0);
 				break;
 			case 2:
-				lineError = playMomLine(0x246f, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x246f, "MOM", 1);
 				break;
 			default:
 				lineError = Common::kNoError;
@@ -638,8 +615,8 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.burnedTvStationShown) {
 		state.burnedTvStationShown = true;
 
-		Common::Error lineError = playSequence(
-			kMomBurnedTvStationOpeningLines, ARRAYSIZE(kMomBurnedTvStationOpeningLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMomBurnedTvStationOpeningLines,
+			ARRAYSIZE(kMomBurnedTvStationOpeningLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -648,25 +625,25 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (responseError.getCode() != Common::kNoError)
 			return responseError;
 		if (responseIndex == 1) {
-			lineError = playMomLine(0x2237, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x2237, "MOM", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 
-		lineError = playSequence(
-			kMomBurnedTvStationPreActionLines, ARRAYSIZE(kMomBurnedTvStationPreActionLines));
+		lineError = runtime.playDialogueEntrySequence(kMomBurnedTvStationPreActionLines,
+			ARRAYSIZE(kMomBurnedTvStationPreActionLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		executeActionTagIfSet(kBabyScreamActionTag);
-		lineError = playSequence(
-			kMomBurnedTvStationPreFstLines, ARRAYSIZE(kMomBurnedTvStationPreFstLines));
+		runtime.executeAndQueueAction(kBabyScreamActionTag);
+		lineError = runtime.playDialogueEntrySequence(kMomBurnedTvStationPreFstLines,
+			ARRAYSIZE(kMomBurnedTvStationPreFstLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		lineError = runtime.playDialogueFst(kDialogueC010FstPath);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playSequence(
-			kMomBurnedTvStationPostFstLines, ARRAYSIZE(kMomBurnedTvStationPostFstLines));
+		lineError = runtime.playDialogueEntrySequence(kMomBurnedTvStationPostFstLines,
+			ARRAYSIZE(kMomBurnedTvStationPostFstLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -674,7 +651,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("SCRATCHED_TUCKER") &&
 			!state.scratchedTuckerShown) {
 		state.scratchedTuckerShown = true;
-		Common::Error lineError = playMomLine(0x23f3, 1);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x23f3, "MOM", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -682,8 +659,8 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("BARBER_POLE_STOLEN") &&
 			!state.barberPoleStolenShown) {
 		state.barberPoleStolenShown = true;
-		Common::Error lineError = playSequence(
-			kMomBarberPoleStolenLines, ARRAYSIZE(kMomBarberPoleStolenLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMomBarberPoleStolenLines,
+			ARRAYSIZE(kMomBarberPoleStolenLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -694,8 +671,8 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		Common::Error lineError = runtime.startupScript().isNamedNpcDeathTypeClear("SPARKY") &&
 				runtime.startupScript().isNamedNpcDeathTypeClear("FIREMAN2")
-			? playSequence(kMomBoltOfClothClearLines, ARRAYSIZE(kMomBoltOfClothClearLines))
-			: playSequence(kMomBoltOfClothDefaultLines, ARRAYSIZE(kMomBoltOfClothDefaultLines));
+			? runtime.playDialogueEntrySequence(kMomBoltOfClothClearLines, ARRAYSIZE(kMomBoltOfClothClearLines))
+			: runtime.playDialogueEntrySequence(kMomBoltOfClothDefaultLines, ARRAYSIZE(kMomBoltOfClothDefaultLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -705,11 +682,11 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.dinerBurnedKarinAliveShown) {
 		state.dinerBurnedKarinAliveShown = true;
 
-		Common::Error lineError = playMomLine(0x2434, 0);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x2434, "MOM", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (sharedState.dialogueStateD2ea4) {
-			lineError = playMomLine(0x2439, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x2439, "MOM", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -720,10 +697,10 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 			switch (responseIndex) {
 			case 1:
-				lineError = playMomLine(0x2448, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2448, "MOM", 0);
 				break;
 			case 2:
-				lineError = playMomLine(0x244d, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x244d, "MOM", 1);
 				break;
 			default:
 				lineError = Common::kNoError;
@@ -737,8 +714,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("PC_ESCAPED_JAIL") &&
 			!state.escapedJailShown) {
 		state.escapedJailShown = true;
-		Common::Error lineError = playSequence(
-			kMomEscapedJailLines, ARRAYSIZE(kMomEscapedJailLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMomEscapedJailLines, ARRAYSIZE(kMomEscapedJailLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -749,7 +725,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.karinKidnapedUnresolvedShown) {
 		state.karinKidnapedUnresolvedShown = true;
 
-		Common::Error lineError = playMomLine(0x24d7, 0);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x24d7, "MOM", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		sharedState.karinKidnapedDialogueState = true;
@@ -761,11 +737,10 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		switch (responseIndex) {
 		case 1:
-			lineError = playMomLine(0x24e3, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x24e3, "MOM", 0);
 			break;
 		case 2:
-			lineError = playSequence(
-				kMomKarinKidnappedResponse2Lines,
+			lineError = runtime.playDialogueEntrySequence(kMomKarinKidnappedResponse2Lines,
 				ARRAYSIZE(kMomKarinKidnappedResponse2Lines));
 			break;
 		default:
@@ -780,8 +755,8 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.karinFoundAliveShown) {
 		state.karinFoundAliveShown = true;
 
-		Common::Error lineError = playSequence(
-			kMomKarinFoundAliveIntroLines, ARRAYSIZE(kMomKarinFoundAliveIntroLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMomKarinFoundAliveIntroLines,
+			ARRAYSIZE(kMomKarinFoundAliveIntroLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -793,13 +768,13 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		switch (responseIndex) {
 		case 1:
 			lineError = runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED")
-				? playSequence(kMomKarinFoundAliveResponse1WithStephMidgameLines,
+				? runtime.playDialogueEntrySequence(kMomKarinFoundAliveResponse1WithStephMidgameLines,
 					ARRAYSIZE(kMomKarinFoundAliveResponse1WithStephMidgameLines))
-				: playSequence(kMomKarinFoundAliveResponse1WithoutStephMidgameLines,
+				: runtime.playDialogueEntrySequence(kMomKarinFoundAliveResponse1WithoutStephMidgameLines,
 					ARRAYSIZE(kMomKarinFoundAliveResponse1WithoutStephMidgameLines));
 			break;
 		case 2:
-			lineError = playMomLine(0x2560, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x2560, "MOM", 2);
 			break;
 		default:
 			lineError = Common::kNoError;
@@ -813,34 +788,34 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.karinFoundDeadShown) {
 		state.karinFoundDeadShown = true;
 
-		Common::Error lineError = playMomLine(0x2576, 0);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x2576, "MOM", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		lineError = hasInventoryObject("K_PURSE")
-			? playPcLine(0x257b, 0)
-			: playPcLine(0x2580, 0);
+			? runtime.playDialogueLineWithVariant(0x257b, "PC", 0)
+			: runtime.playDialogueLineWithVariant(0x2580, "PC", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playPcLine(0x2585, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x2585, "PC", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playMomLine(0x258b, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x258b, "MOM", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (currentStoryDayIndex <= 5) {
-			lineError = playMomLine(0x258f, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x258f, "MOM", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playPcLine(0x2594, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x2594, "PC", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playMomLine(0x2598, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x2598, "MOM", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 		if (runtime.startupScript().getFlagValue("DINER_BURNED") &&
 				!runtime.startupScript().isNamedNpcDeathTypeClear("EDNA")) {
-			lineError = playMomLine(0x25a0, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x25a0, "MOM", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -849,7 +824,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (!runtime.startupScript().isNamedNpcDeathTypeClear("BUTCHER") &&
 			!state.butcherAbsentShown) {
 		state.butcherAbsentShown = true;
-		Common::Error lineError = playMomLine(0x2633, 0);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x2633, "MOM", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -857,16 +832,16 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (!runtime.startupScript().isNamedNpcDeathTypeClear("MOYNAHAN") &&
 			!state.moynahanAbsentShown) {
 		state.moynahanAbsentShown = true;
-		Common::Error lineError = playSequence(
-			kMomMoynahanAbsentLines, ARRAYSIZE(kMomMoynahanAbsentLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMomMoynahanAbsentLines,
+			ARRAYSIZE(kMomMoynahanAbsentLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (!runtime.startupScript().isNamedNpcDeathTypeClear("JIMMY")) {
 		Common::Error lineError = runtime.startupScript().getFlagValue("JIMMY_ATTACKED")
-			? playSequence(kMomJimmyAttackedLines, ARRAYSIZE(kMomJimmyAttackedLines))
-			: playMomLine(0x266c, 0);
+			? runtime.playDialogueEntrySequence(kMomJimmyAttackedLines, ARRAYSIZE(kMomJimmyAttackedLines))
+			: runtime.playDialogueLineWithVariant(0x266c, "MOM", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		if (!runtime.startupScript().getFlagValue("JIMMY_ATTACKED")) {
@@ -877,13 +852,13 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 			switch (responseIndex) {
 			case 1:
-				lineError = playMomLine(0x2677, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2677, "MOM", 0);
 				break;
 			case 2:
-				lineError = playMomLine(0x267b, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x267b, "MOM", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playMomLine(0x267f, 3);
+				lineError = runtime.playDialogueLineWithVariant(0x267f, "MOM", 3);
 				break;
 			default:
 				lineError = Common::kNoError;
@@ -897,7 +872,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (!runtime.startupScript().isNamedNpcDeathTypeClear("WASP_WOMAN") &&
 			!state.waspWomanAbsentShown) {
 		state.waspWomanAbsentShown = true;
-		Common::Error lineError = playMomLine(0x2689, 1);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x2689, "MOM", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -906,8 +881,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED") &&
 			!state.stephanieDeadPreMidgameShown) {
 		state.stephanieDeadPreMidgameShown = true;
-		Common::Error lineError = playSequence(
-			kMomStephanieDeadPreMidgameLines,
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMomStephanieDeadPreMidgameLines,
 			ARRAYSIZE(kMomStephanieDeadPreMidgameLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
@@ -916,7 +890,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("DAY_5") && !state.day5Shown) {
 		state.day5Shown = true;
 
-		Common::Error lineError = playSequence(kMomDay5Lines, ARRAYSIZE(kMomDay5Lines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMomDay5Lines, ARRAYSIZE(kMomDay5Lines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -927,10 +901,10 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		switch (responseIndex) {
 		case 1:
-			lineError = playMomLine(0x20ce, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x20ce, "MOM", 1);
 			break;
 		case 2:
-			lineError = playMomLine(0x2201, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x2201, "MOM", 0);
 			break;
 		default:
 			lineError = Common::kNoError;
@@ -938,16 +912,16 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		assignMomTopicBuffer(kMomDay5TopicBufferResponseLine);
+		topics.assign(kMomDay5TopicBufferResponseLine);
 	}
 
 	if (runtime.startupScript().getFlagValue("DAY_6") && !state.day6Shown) {
 		state.day6Shown = true;
 
-		Common::Error lineError = playSequence(kMomDay6Lines, ARRAYSIZE(kMomDay6Lines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMomDay6Lines, ARRAYSIZE(kMomDay6Lines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		assignMomTopicBuffer(kMomDay6TopicBufferResponseLine);
+		topics.assign(kMomDay6TopicBufferResponseLine);
 	}
 
 	for (;;) {
@@ -962,147 +936,144 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return runMomGoodbye();
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x116)) {
-			Common::Error lineError = playSequence(kMomCookiesLines, ARRAYSIZE(kMomCookiesLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomCookiesLines, ARRAYSIZE(kMomCookiesLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(0x118);
+			topics.assign(0x118);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x119)) {
-			Common::Error lineError = playSequence(kMomTopic119Lines, ARRAYSIZE(kMomTopic119Lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic119Lines, ARRAYSIZE(kMomTopic119Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(0x11a);
+			topics.assign(0x11a);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x11d)) {
-			Common::Error lineError = playSequence(
-				kMomTopic11dOpeningLines, ARRAYSIZE(kMomTopic11dOpeningLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic11dOpeningLines,
+				ARRAYSIZE(kMomTopic11dOpeningLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC008AFstPath);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playSequence(
-				kMomTopic11dPostC008ALines, ARRAYSIZE(kMomTopic11dPostC008ALines));
+			lineError = runtime.playDialogueEntrySequence(kMomTopic11dPostC008ALines, ARRAYSIZE(kMomTopic11dPostC008ALines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			executeActionTagIfSet(kBabyGurgleActionTag);
-			lineError = playSequence(
-				kMomTopic11dClosingLines, ARRAYSIZE(kMomTopic11dClosingLines));
+			runtime.executeAndQueueAction(kBabyGurgleActionTag);
+			lineError = runtime.playDialogueEntrySequence(kMomTopic11dClosingLines, ARRAYSIZE(kMomTopic11dClosingLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			lineError = runtime.playDialogueFst(kDialogueC008BFstPath);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(0x120);
+			topics.assign(0x120);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x121)) {
-			Common::Error lineError = playSequence(kMomTopic121Lines, ARRAYSIZE(kMomTopic121Lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic121Lines, ARRAYSIZE(kMomTopic121Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			sharedState.waspWomanDialogueState = true;
-			assignMomTopicBuffer(0x122);
+			topics.assign(0x122);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x123)) {
-			Common::Error lineError = playSequence(kMomSparkyLines, ARRAYSIZE(kMomSparkyLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomSparkyLines, ARRAYSIZE(kMomSparkyLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(0x124);
+			topics.assign(0x124);
 			continue;
 		}
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMomPtaTopicResponseLines,
 				ARRAYSIZE(kMomPtaTopicResponseLines))) {
-			Common::Error lineError = playSequence(kMomPtaLines, ARRAYSIZE(kMomPtaLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomPtaLines, ARRAYSIZE(kMomPtaLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(kMomPtaFollowupTopicBufferResponseLine);
+			topics.assign(kMomPtaFollowupTopicBufferResponseLine);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x12b)) {
-			Common::Error lineError = playSequence(kMomTopic12bLines, ARRAYSIZE(kMomTopic12bLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic12bLines, ARRAYSIZE(kMomTopic12bLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(0x12c);
+			topics.assign(0x12c);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x12d)) {
-			Common::Error lineError = playSequence(kMomTopic12dLines, ARRAYSIZE(kMomTopic12dLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic12dLines, ARRAYSIZE(kMomTopic12dLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(0x12e);
+			topics.assign(0x12e);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x12f)) {
-			Common::Error lineError = playSequence(kMomTopic12fLines, ARRAYSIZE(kMomTopic12fLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic12fLines, ARRAYSIZE(kMomTopic12fLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(0x130);
+			topics.assign(0x130);
 			continue;
 		}
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMomCookingTopicResponseLines,
 				ARRAYSIZE(kMomCookingTopicResponseLines))) {
-			Common::Error lineError = playSequence(kMomCookingLines, ARRAYSIZE(kMomCookingLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomCookingLines, ARRAYSIZE(kMomCookingLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(kMomCookingFollowupTopicBufferResponseLine);
+			topics.assign(kMomCookingFollowupTopicBufferResponseLine);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x135)) {
-			Common::Error lineError = playSequence(kMomTopic135Lines, ARRAYSIZE(kMomTopic135Lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic135Lines, ARRAYSIZE(kMomTopic135Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(0x136);
+			topics.assign(0x136);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x137)) {
-			Common::Error lineError = playSequence(kMomTopic137Lines, ARRAYSIZE(kMomTopic137Lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic137Lines, ARRAYSIZE(kMomTopic137Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x13a)) {
-			Common::Error lineError = playMomLine(0x1fa3, 0);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x1fa3, "MOM", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(0x13b);
+			topics.assign(0x13b);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x13c)) {
-			Common::Error lineError = playSequence(
-				kMomSlaughterhouseLines, ARRAYSIZE(kMomSlaughterhouseLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomSlaughterhouseLines,
+				ARRAYSIZE(kMomSlaughterhouseLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(kMomSlaughterhouseFollowupTopicBufferResponseLine);
+			topics.assign(kMomSlaughterhouseFollowupTopicBufferResponseLine);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x13f)) {
 			sharedState.momFatherTopicState = 1;
-			Common::Error lineError = playSequence(kMomFatherLines, ARRAYSIZE(kMomFatherLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomFatherLines, ARRAYSIZE(kMomFatherLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			if (runtime.startupScript().getFlagValue("HEARD_DAD_MOAN")) {
-				lineError = playSequence(
-					kMomFatherHeardDadMoanLines, ARRAYSIZE(kMomFatherHeardDadMoanLines));
+				lineError = runtime.playDialogueEntrySequence(kMomFatherHeardDadMoanLines, ARRAYSIZE(kMomFatherHeardDadMoanLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
-			assignMomTopicBuffer(kMomFatherFollowupTopicBufferResponseLine);
+			topics.assign(kMomFatherFollowupTopicBufferResponseLine);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x141)) {
 			if (runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED")) {
-				Common::Error lineError = playSequence(
-					kMomStephMidgameLodgeLines, ARRAYSIZE(kMomStephMidgameLodgeLines));
+				Common::Error lineError = runtime.playDialogueEntrySequence(kMomStephMidgameLodgeLines,
+					ARRAYSIZE(kMomStephMidgameLodgeLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				continue;
 			}
 
 			sharedState.discussedLodgeTopic = true;
-			Common::Error lineError = playSequence(kMomLodgeLines, ARRAYSIZE(kMomLodgeLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomLodgeLines, ARRAYSIZE(kMomLodgeLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -1113,16 +1084,16 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 			switch (responseIndex) {
 			case 1:
-				lineError = playMomLine(0x1ffe, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x1ffe, "MOM", 1);
 				break;
 			case 2:
-				lineError = playMomLine(0x2005, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x2005, "MOM", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playPcLine(0x200a, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x200a, "PC", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playMomLine(0x200e, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x200e, "MOM", 0);
 				break;
 			default:
 				lineError = Common::kNoError;
@@ -1130,19 +1101,19 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(kMomLodgeFollowupTopicBufferResponseLine);
+			topics.assign(kMomLodgeFollowupTopicBufferResponseLine);
 			continue;
 		}
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMomBakeSaleTopicResponseLines,
 				ARRAYSIZE(kMomBakeSaleTopicResponseLines))) {
-			Common::Error lineError = playSequence(kMomBakeSaleLines, ARRAYSIZE(kMomBakeSaleLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomBakeSaleLines, ARRAYSIZE(kMomBakeSaleLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignMomTopicBuffer(kMomBakeSaleFollowupTopicBufferResponseLine);
+			topics.assign(kMomBakeSaleFollowupTopicBufferResponseLine);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x14a)) {
-			Common::Error lineError = playSequence(kMomTopic14aLines, ARRAYSIZE(kMomTopic14aLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic14aLines, ARRAYSIZE(kMomTopic14aLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
@@ -1150,7 +1121,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMomPottsdamTopicResponseLines,
 				ARRAYSIZE(kMomPottsdamTopicResponseLines)) &&
 				runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED")) {
-			Common::Error lineError = playMomLine(0x2086, 0);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x2086, "MOM", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -1161,13 +1132,11 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 			switch (responseIndex) {
 			case 1:
-				lineError = playSequence(
-					kMomPottsdamFirstResponse1Lines,
+				lineError = runtime.playDialogueEntrySequence(kMomPottsdamFirstResponse1Lines,
 					ARRAYSIZE(kMomPottsdamFirstResponse1Lines));
 				break;
 			case 2:
-				lineError = playSequence(
-					kMomPottsdamFirstResponse2Lines,
+				lineError = runtime.playDialogueEntrySequence(kMomPottsdamFirstResponse2Lines,
 					ARRAYSIZE(kMomPottsdamFirstResponse2Lines));
 				break;
 			default:
@@ -1177,8 +1146,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			lineError = playSequence(
-				kMomPottsdamCommonFollowupLines,
+			lineError = runtime.playDialogueEntrySequence(kMomPottsdamCommonFollowupLines,
 				ARRAYSIZE(kMomPottsdamCommonFollowupLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -1190,8 +1158,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 			switch (responseIndex) {
 			case 1:
-				lineError = playSequence(
-					kMomPottsdamSecondResponse1Lines,
+				lineError = runtime.playDialogueEntrySequence(kMomPottsdamSecondResponse1Lines,
 					ARRAYSIZE(kMomPottsdamSecondResponse1Lines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
@@ -1203,13 +1170,11 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 				switch (responseIndex) {
 				case 1:
-					lineError = playSequence(
-						kMomPottsdamThirdResponse1Lines,
+					lineError = runtime.playDialogueEntrySequence(kMomPottsdamThirdResponse1Lines,
 						ARRAYSIZE(kMomPottsdamThirdResponse1Lines));
 					break;
 				case 2:
-					lineError = playSequence(
-						kMomPottsdamThirdResponse2Lines,
+					lineError = runtime.playDialogueEntrySequence(kMomPottsdamThirdResponse2Lines,
 						ARRAYSIZE(kMomPottsdamThirdResponse2Lines));
 					break;
 				default:
@@ -1218,8 +1183,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				}
 				break;
 			case 2:
-				lineError = playSequence(
-					kMomPottsdamSecondResponse2Lines,
+				lineError = runtime.playDialogueEntrySequence(kMomPottsdamSecondResponse2Lines,
 					ARRAYSIZE(kMomPottsdamSecondResponse2Lines));
 				break;
 			default:
@@ -1229,13 +1193,13 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignMomTopicBuffer(kMomPottsdamFollowupTopicBufferResponseLine);
+			topics.assign(kMomPottsdamFollowupTopicBufferResponseLine);
 			continue;
 		}
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMomMeatPlantTopicResponseLines,
 				ARRAYSIZE(kMomMeatPlantTopicResponseLines))) {
-			Common::Error lineError = playSequence(
-				kMomMeatPlantIntroLines, ARRAYSIZE(kMomMeatPlantIntroLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomMeatPlantIntroLines,
+				ARRAYSIZE(kMomMeatPlantIntroLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -1246,14 +1210,10 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 			switch (responseIndex) {
 			case 1:
-				lineError = playSequence(
-					kMomMeatPlantResponse1Lines,
-					ARRAYSIZE(kMomMeatPlantResponse1Lines));
+				lineError = runtime.playDialogueEntrySequence(kMomMeatPlantResponse1Lines, ARRAYSIZE(kMomMeatPlantResponse1Lines));
 				break;
 			case 2:
-				lineError = playSequence(
-					kMomMeatPlantResponse2Lines,
-					ARRAYSIZE(kMomMeatPlantResponse2Lines));
+				lineError = runtime.playDialogueEntrySequence(kMomMeatPlantResponse2Lines, ARRAYSIZE(kMomMeatPlantResponse2Lines));
 				break;
 			default:
 				lineError = Common::kNoError;
@@ -1262,66 +1222,63 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			if (responseIndex == 2)
-				assignMomTopicBuffer(kMomMeatPlantFollowupTopicBufferResponseLine);
+				topics.assign(kMomMeatPlantFollowupTopicBufferResponseLine);
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x155)) {
-			Common::Error lineError = playSequence(kMomTopic155Lines, ARRAYSIZE(kMomTopic155Lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic155Lines, ARRAYSIZE(kMomTopic155Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x156)) {
-			Common::Error lineError = playSequence(kMomTopic156Lines, ARRAYSIZE(kMomTopic156Lines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic156Lines, ARRAYSIZE(kMomTopic156Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x15b) && currentStoryDayIndex == 5) {
 			Common::Error lineError = sharedState.momGoodCauseDay5State
-				? playSequence(kMomGoodCauseDay5CompleteLines,
-					ARRAYSIZE(kMomGoodCauseDay5CompleteLines))
-				: playSequence(kMomGoodCauseDay5FirstTimeLines,
-					ARRAYSIZE(kMomGoodCauseDay5FirstTimeLines));
+				? runtime.playDialogueEntrySequence(kMomGoodCauseDay5CompleteLines, ARRAYSIZE(kMomGoodCauseDay5CompleteLines))
+				: runtime.playDialogueEntrySequence(kMomGoodCauseDay5FirstTimeLines, ARRAYSIZE(kMomGoodCauseDay5FirstTimeLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			if (sharedState.momGoodCauseDay5State &&
 					runtime.startupScript().getFlagValue(kSawSlashesOnBodyFlag)) {
-				lineError = playSequence(
-					kMomGoodCauseDay5SlashBodyLines,
+				lineError = runtime.playDialogueEntrySequence(kMomGoodCauseDay5SlashBodyLines,
 					ARRAYSIZE(kMomGoodCauseDay5SlashBodyLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 
 			if (!sharedState.momGoodCauseDay5State)
-				assignMomTopicBuffer(kMomGoodCauseDay5FollowupTopicBufferResponseLine);
+				topics.assign(kMomGoodCauseDay5FollowupTopicBufferResponseLine);
 			continue;
 		}
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMomMoynahanTopicResponseLines,
 				ARRAYSIZE(kMomMoynahanTopicResponseLines))) {
-			Common::Error lineError = playSequence(
-				kMomMoynahanTopicLines, ARRAYSIZE(kMomMoynahanTopicLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomMoynahanTopicLines,
+				ARRAYSIZE(kMomMoynahanTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
 		}
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMomNewspaperFireTopicResponseLines,
 				ARRAYSIZE(kMomNewspaperFireTopicResponseLines))) {
-			Common::Error lineError = playMomLine(0x229e, 0);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x229e, "MOM", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x16d)) {
-			Common::Error lineError = playSequence(kMomTopic16dLines, ARRAYSIZE(kMomTopic16dLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMomTopic16dLines, ARRAYSIZE(kMomTopic16dLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x16e)) {
-			Common::Error lineError = playMomLine(0x26a3, 0);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x26a3, "MOM", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -1332,10 +1289,10 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 			switch (responseIndex) {
 			case 1:
-				lineError = playMomLine(0x26ae, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x26ae, "MOM", 0);
 				break;
 			case 2:
-				lineError = playMomLine(0x26b3, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x26b3, "MOM", 2);
 				break;
 			default:
 				lineError = Common::kNoError;
@@ -1346,7 +1303,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return runMomGoodbye();
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x170)) {
-			Common::Error lineError = playMomLine(0x26bc, 2);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x26bc, "MOM", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			continue;
@@ -1354,7 +1311,7 @@ Common::Error MomDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesResponseLine(selectedTopic, 0x171))
 			continue;
 
-		Common::Error lineError = playMomLine(0x26c6, 1);
+		Common::Error lineError = runtime.playDialogueLineWithVariant(0x26c6, "MOM", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		continue;
diff --git a/engines/harvester/npc/mom_dialogue.h b/engines/harvester/npc/mom_dialogue.h
index 701ddab590a..60bedb30227 100644
--- a/engines/harvester/npc/mom_dialogue.h
+++ b/engines/harvester/npc/mom_dialogue.h
@@ -52,7 +52,6 @@ public:
 		bool day6Shown = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = MomRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.introPending);
diff --git a/engines/harvester/npc/mother_dialogue.cpp b/engines/harvester/npc/mother_dialogue.cpp
index e21c70bcb19..c526c750ff0 100644
--- a/engines/harvester/npc/mother_dialogue.cpp
+++ b/engines/harvester/npc/mother_dialogue.cpp
@@ -28,28 +28,19 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kMotherSpeakerId = "MOTHER";
 static const char *const kDialogueC072FstPath = "GRAPHIC/FST/C072.FST";
 static const char *const kDialogueC073FstPath = "GRAPHIC/FST/C073.FST";
 
 } // End of namespace
 
-bool MotherDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase("MOTHER");
-}
-
 Common::Error MotherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &, DialogueSharedState &) {
 	if (!_state.talkStatePending)
 		return Common::kNoError;
 
-	auto playMotherLine = [&](int wavId, int headVariant) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kMotherSpeakerId, headVariant);
-	};
-
 	_state.talkStatePending = false;
 
-	Common::Error lineError = playMotherLine(0x26d6, 3);
+	Common::Error lineError = runtime.playDialogueLineWithVariant(0x26d6, "MOTHER", 3);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -57,7 +48,7 @@ Common::Error MotherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playMotherLine(0x26e9, 0);
+	lineError = runtime.playDialogueLineWithVariant(0x26e9, "MOTHER", 0);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -67,7 +58,7 @@ Common::Error MotherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		return responseError;
 
 	if (responseIndex == 1) {
-		lineError = playMotherLine(0x26f5, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x26f5, "MOTHER", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -81,7 +72,7 @@ Common::Error MotherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playMotherLine(0x2709, 3);
+	lineError = runtime.playDialogueLineWithVariant(0x2709, "MOTHER", 3);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
@@ -89,11 +80,11 @@ Common::Error MotherDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	lineError = playMotherLine(0x2714, 0);
+	lineError = runtime.playDialogueLineWithVariant(0x2714, "MOTHER", 0);
 	if (lineError.getCode() != Common::kNoError)
 		return lineError;
 
-	return playMotherLine(0x2719, 3);
+	return runtime.playDialogueLineWithVariant(0x2719, "MOTHER", 3);
 }
 
 } // End of namespace Harvester
diff --git a/engines/harvester/npc/mother_dialogue.h b/engines/harvester/npc/mother_dialogue.h
index b52827e2b7b..31567739e43 100644
--- a/engines/harvester/npc/mother_dialogue.h
+++ b/engines/harvester/npc/mother_dialogue.h
@@ -32,7 +32,6 @@ public:
 		bool talkStatePending = true;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = MotherRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.talkStatePending);
diff --git a/engines/harvester/npc/moynahan_dialogue.cpp b/engines/harvester/npc/moynahan_dialogue.cpp
index e7c94b85c1b..0482531fb42 100644
--- a/engines/harvester/npc/moynahan_dialogue.cpp
+++ b/engines/harvester/npc/moynahan_dialogue.cpp
@@ -29,9 +29,6 @@ namespace Harvester {
 
 namespace {
 
-static const char *const kMoynahanNpc = "MOYNAHAN";
-static const char *const kMoynahanEmbalmingPortrait = "MOYNAHAN_MBLM";
-static const char *const kPcSpeaker = "PC";
 static const char *const kMoynahanHotelRoom = "HTL-RT";
 static const char *const kMoynahanEmbalmingRoom = "MBLM";
 static const char *const kRahRoom = "RAH";
@@ -48,185 +45,166 @@ static const int kMoynahanLateTopicLines[] = { 0x1b8, 0x1b9, 0x1ba, 0x1bb, 0x1bc
 static const int kMoynahanGlueTopicLines[] = { 0x1bf, 0x1c0, 0x1c1, 0x1c2 };
 
 static const DialogueLineEntry kMoynahanGlueInterruptOpeningLines[] = {
-	{ 0x3e4d, kMoynahanNpc, 2 },
-	{ 0x3e52, kPcSpeaker, 0 },
-	{ 0x3e56, kMoynahanNpc, 0 }
+	{ 0x3e4d, "MOYNAHAN", 2 },
+	{ 0x3e52, "PC", 0 },
+	{ 0x3e56, "MOYNAHAN", 0 }
 };
 
 static const DialogueLineEntry kMoynahanGlueInterruptShortReplyLines[] = {
-	{ 0x3e76, kPcSpeaker, 0 },
-	{ 0x3e7a, kMoynahanNpc, 0 }
+	{ 0x3e76, "PC", 0 },
+	{ 0x3e7a, "MOYNAHAN", 0 }
 };
 
 static const DialogueLineEntry kMoynahanGlueInterruptExtendedReplyLines[] = {
-	{ 0x3e5b, kPcSpeaker, 0 },
-	{ 0x3e60, kMoynahanNpc, 0 },
-	{ 0x3e64, kPcSpeaker, 2 },
-	{ 0x3e69, kMoynahanNpc, 0 },
-	{ 0x3e6d, kPcSpeaker, 2 },
-	{ 0x3e71, kMoynahanNpc, 2 }
+	{ 0x3e5b, "PC", 0 },
+	{ 0x3e60, "MOYNAHAN", 0 },
+	{ 0x3e64, "PC", 2 },
+	{ 0x3e69, "MOYNAHAN", 0 },
+	{ 0x3e6d, "PC", 2 },
+	{ 0x3e71, "MOYNAHAN", 2 }
 };
 
 static const DialogueLineEntry kMoynahanIntroLines[] = {
-	{ 0x3d33, kMoynahanNpc, 0 },
-	{ 0x3d37, kPcSpeaker, 0 },
-	{ 0x3d3b, kMoynahanNpc, 0 },
-	{ 0x3d40, kPcSpeaker, 0 },
-	{ 0x3d45, kMoynahanNpc, 0 },
-	{ 0x3d47, kMoynahanNpc, 0 }
+	{ 0x3d33, "MOYNAHAN", 0 },
+	{ 0x3d37, "PC", 0 },
+	{ 0x3d3b, "MOYNAHAN", 0 },
+	{ 0x3d40, "PC", 0 },
+	{ 0x3d45, "MOYNAHAN", 0 },
+	{ 0x3d47, "MOYNAHAN", 0 }
 };
 
 static const DialogueLineEntry kMoynahanSlashMarksIntroLines[] = {
-	{ 0x3da0, kMoynahanNpc, 3 },
-	{ 0x3da1, kMoynahanNpc, 3 },
-	{ 0x3da2, kMoynahanNpc, 2 }
+	{ 0x3da0, "MOYNAHAN", 3 },
+	{ 0x3da1, "MOYNAHAN", 3 },
+	{ 0x3da2, "MOYNAHAN", 2 }
 };
 
 static const DialogueLineEntry kMoynahanDayFiveLines[] = {
-	{ 0x3fe5, kMoynahanNpc, 1 },
-	{ 0x3fea, kPcSpeaker, 0 },
-	{ 0x3fee, kMoynahanNpc, 2 },
-	{ 0x3ff3, kPcSpeaker, 0 },
-	{ 0x3ff7, kMoynahanNpc, 0 }
+	{ 0x3fe5, "MOYNAHAN", 1 },
+	{ 0x3fea, "PC", 0 },
+	{ 0x3fee, "MOYNAHAN", 2 },
+	{ 0x3ff3, "PC", 0 },
+	{ 0x3ff7, "MOYNAHAN", 0 }
 };
 
 static const DialogueLineEntry kMoynahanKarinKidnapedTailLines[] = {
-	{ 0x4020, kMoynahanNpc, 0 },
-	{ 0x4026, kPcSpeaker, 0 },
-	{ 0x402a, kMoynahanNpc, 0 },
-	{ 0x4034, kPcSpeaker, 0 },
-	{ 0x4038, kMoynahanNpc, 0 },
-	{ 0x403d, kPcSpeaker, 0 },
-	{ 0x4041, kMoynahanNpc, 1 }
+	{ 0x4020, "MOYNAHAN", 0 },
+	{ 0x4026, "PC", 0 },
+	{ 0x402a, "MOYNAHAN", 0 },
+	{ 0x4034, "PC", 0 },
+	{ 0x4038, "MOYNAHAN", 0 },
+	{ 0x403d, "PC", 0 },
+	{ 0x4041, "MOYNAHAN", 1 }
 };
 
 static const DialogueLineEntry kMoynahanCasketIntroLines[] = {
-	{ 0x3e84, kMoynahanNpc, 0 },
-	{ 0x3e89, kPcSpeaker, 0 },
-	{ 0x3e8e, kMoynahanNpc, 0 },
-	{ 0x3e93, kPcSpeaker, 0 },
-	{ 0x3e98, kMoynahanNpc, 2 }
+	{ 0x3e84, "MOYNAHAN", 0 },
+	{ 0x3e89, "PC", 0 },
+	{ 0x3e8e, "MOYNAHAN", 0 },
+	{ 0x3e93, "PC", 0 },
+	{ 0x3e98, "MOYNAHAN", 2 }
 };
 
 static const DialogueLineEntry kMoynahanCasketResponseOneLines[] = {
-	{ 0x3ea4, kMoynahanNpc, 0 },
-	{ 0x3ea8, kPcSpeaker, 2 },
-	{ 0x3ead, kMoynahanNpc, 2 }
+	{ 0x3ea4, "MOYNAHAN", 0 },
+	{ 0x3ea8, "PC", 2 },
+	{ 0x3ead, "MOYNAHAN", 2 }
 };
 
 static const DialogueLineEntry kMoynahanCasketFollowupLines[] = {
-	{ 0x3eb7, kPcSpeaker, 2 },
-	{ 0x3ebc, kMoynahanNpc, 2 }
+	{ 0x3eb7, "PC", 2 },
+	{ 0x3ebc, "MOYNAHAN", 2 }
 };
 
 static const DialogueLineEntry kMoynahanLedgerIntroLines[] = {
-	{ 0x3ede, kPcSpeaker, 0 },
-	{ 0x3ee3, kMoynahanNpc, 0 },
-	{ 0x3ee7, kPcSpeaker, 0 },
-	{ 0x3eec, kMoynahanNpc, 0 }
+	{ 0x3ede, "PC", 0 },
+	{ 0x3ee3, "MOYNAHAN", 0 },
+	{ 0x3ee7, "PC", 0 },
+	{ 0x3eec, "MOYNAHAN", 0 }
 };
 
 static const DialogueLineEntry kMoynahanLedgerResponseOneLines[] = {
-	{ 0x3ef7, kMoynahanNpc, 0 },
-	{ 0x3efb, kPcSpeaker, 0 },
-	{ 0x3eff, kMoynahanNpc, 2 },
-	{ 0x3f05, kPcSpeaker, 2 }
+	{ 0x3ef7, "MOYNAHAN", 0 },
+	{ 0x3efb, "PC", 0 },
+	{ 0x3eff, "MOYNAHAN", 2 },
+	{ 0x3f05, "PC", 2 }
 };
 
 static const DialogueLineEntry kMoynahanLedgerResponseTwoLines[] = {
-	{ 0x3f09, kMoynahanNpc, 0 },
-	{ 0x3f0f, kPcSpeaker, 2 },
-	{ 0x3f13, kMoynahanNpc, 0 },
-	{ 0x3f18, kPcSpeaker, 2 }
+	{ 0x3f09, "MOYNAHAN", 0 },
+	{ 0x3f0f, "PC", 2 },
+	{ 0x3f13, "MOYNAHAN", 0 },
+	{ 0x3f18, "PC", 2 }
 };
 
 static const DialogueLineEntry kMoynahanMatchesLines[] = {
-	{ 0x4049, kPcSpeaker, 0 },
-	{ 0x404d, kMoynahanNpc, 0 }
+	{ 0x4049, "PC", 0 },
+	{ 0x404d, "MOYNAHAN", 0 }
 };
 
 static const DialogueLineEntry kMoynahanGoodCauseLines[] = {
-	{ 0x3d5f, kMoynahanNpc, 0 },
-	{ 0x3d66, kPcSpeaker, 0 },
-	{ 0x3d6a, kMoynahanNpc, 0 }
+	{ 0x3d5f, "MOYNAHAN", 0 },
+	{ 0x3d66, "PC", 0 },
+	{ 0x3d6a, "MOYNAHAN", 0 }
 };
 
 static const DialogueLineEntry kMoynahanSlashMarksTopicReplyLines[] = {
-	{ 0x3d76, kMoynahanNpc, 0 },
-	{ 0x3d7d, kPcSpeaker, 0 },
-	{ 0x3d81, kMoynahanNpc, 0 },
-	{ 0x3d88, kPcSpeaker, 0 },
-	{ 0x3d8c, kMoynahanNpc, 0 },
-	{ 0x3d92, kPcSpeaker, 0 },
-	{ 0x3d96, kMoynahanNpc, 0 }
+	{ 0x3d76, "MOYNAHAN", 0 },
+	{ 0x3d7d, "PC", 0 },
+	{ 0x3d81, "MOYNAHAN", 0 },
+	{ 0x3d88, "PC", 0 },
+	{ 0x3d8c, "MOYNAHAN", 0 },
+	{ 0x3d92, "PC", 0 },
+	{ 0x3d96, "MOYNAHAN", 0 }
 };
 
 static const DialogueLineEntry kMoynahanGeneralTopicResponseOneLines[] = {
-	{ 0x3dbe, kMoynahanNpc, 1 },
-	{ 0x3dc4, kPcSpeaker, 0 },
-	{ 0x3dc8, kMoynahanNpc, 2 }
+	{ 0x3dbe, "MOYNAHAN", 1 },
+	{ 0x3dc4, "PC", 0 },
+	{ 0x3dc8, "MOYNAHAN", 2 }
 };
 
 static const DialogueLineEntry kMoynahanGeneralTopicResponseTwoLines[] = {
-	{ 0x3dcf, kMoynahanNpc, 0 },
-	{ 0x3dd4, kPcSpeaker, 0 },
-	{ 0x3dd8, kMoynahanNpc, 1 },
-	{ 0x3ddd, kPcSpeaker, 0 },
-	{ 0x3de2, kMoynahanNpc, 2 }
+	{ 0x3dcf, "MOYNAHAN", 0 },
+	{ 0x3dd4, "PC", 0 },
+	{ 0x3dd8, "MOYNAHAN", 1 },
+	{ 0x3ddd, "PC", 0 },
+	{ 0x3de2, "MOYNAHAN", 2 }
 };
 
 static const DialogueLineEntry kMoynahanDayFiveUnlockLines[] = {
-	{ 0x3df5, kMoynahanNpc, 3 },
-	{ 0x3dfc, kPcSpeaker, 0 },
-	{ 0x3e00, kMoynahanNpc, 0 },
-	{ 0x3e05, kPcSpeaker, 0 },
-	{ 0x3e0a, kMoynahanNpc, 0 }
+	{ 0x3df5, "MOYNAHAN", 3 },
+	{ 0x3dfc, "PC", 0 },
+	{ 0x3e00, "MOYNAHAN", 0 },
+	{ 0x3e05, "PC", 0 },
+	{ 0x3e0a, "MOYNAHAN", 0 }
 };
 
 static const DialogueLineEntry kMoynahanEmbalmingRoomLines[] = {
-	{ 0x3e18, kMoynahanNpc, 0 },
-	{ 0x3e22, kPcSpeaker, 0 },
-	{ 0x3e26, kMoynahanNpc, 2 }
+	{ 0x3e18, "MOYNAHAN", 0 },
+	{ 0x3e22, "PC", 0 },
+	{ 0x3e26, "MOYNAHAN", 2 }
 };
 
-static Common::Error playDialogueSequence(DialogueRuntime &runtime,
-		const DialogueLineEntry *lines, uint count) {
-	return runtime.playDialogueEntrySequence(lines, count);
-}
-
 } // End of namespace
 
-bool MoynahanDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase(kMoynahanNpc) ||
-		npcName.equalsIgnoreCase(kMoynahanEmbalmingPortrait);
-}
-
 Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	MoynahanRoomDialogueState &state = _state;
 	Common::String topicBuffer;
 	int topicBufferLineIndex = -1;
 
-	auto assignTopicBuffer = [&](int responseLineIndex) {
-		runtime.assignTopicBuffer(topicBuffer, topicBufferLineIndex,
-			responseLineIndex, "Moynahan topic buffer");
-	};
+	DialogueTopicBuffer topics(runtime, topicBuffer, topicBufferLineIndex, "Moynahan topic buffer");
 	auto appendTopicBuffer = [&](int responseLineIndex) {
 		if (topicBuffer.empty()) {
-			assignTopicBuffer(responseLineIndex);
+			topics.assign(responseLineIndex);
 			return;
 		}
 
 		topicBuffer += runtime.startupText().getDialogueResponseLine(responseLineIndex);
 		topicBufferLineIndex = -1;
 	};
-	auto clearTopicBuffer = [&]() {
-		topicBuffer.clear();
-		topicBufferLineIndex = -1;
-	};
-	auto playMoynahanLine = [&](int wavId, int headVariant = 0) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, kMoynahanNpc, headVariant);
-	};
+
 	auto queueRoomVisualMutation = [&](bool changed) {
 		if (!changed)
 			return;
@@ -260,32 +238,30 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	};
 
 	if (runtime.startupScript().getFlagValue("IF_TRY_TO_TAKE_THE_GLUE")) {
-		Common::Error lineError = playDialogueSequence(runtime,
-			kMoynahanGlueInterruptOpeningLines, ARRAYSIZE(kMoynahanGlueInterruptOpeningLines));
+		Common::Error lineError = runtime.playDialogueEntrySequence(kMoynahanGlueInterruptOpeningLines,
+			ARRAYSIZE(kMoynahanGlueInterruptOpeningLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
 		if (!runtime.startupScript().getFlagValue("IF_SAW_THE_SLASHES_ON_THE_BODY") ||
 				!state.slashMarksTopicUnlocked) {
-			return playDialogueSequence(runtime,
-				kMoynahanGlueInterruptShortReplyLines,
+			return runtime.playDialogueEntrySequence(kMoynahanGlueInterruptShortReplyLines,
 				ARRAYSIZE(kMoynahanGlueInterruptShortReplyLines));
 		}
 
-		return playDialogueSequence(runtime,
-			kMoynahanGlueInterruptExtendedReplyLines,
+		return runtime.playDialogueEntrySequence(kMoynahanGlueInterruptExtendedReplyLines,
 			ARRAYSIZE(kMoynahanGlueInterruptExtendedReplyLines));
 	}
 
 	if (!usedItemName.empty()) {
 		if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
 			sharedState.discussedWhaleyHerrillPhoto = 1;
-			return playMoynahanLine(0x3e45);
+			return runtime.playDialogueLineWithVariant(0x3e45, "MOYNAHAN", 0);
 		}
 
 		if (isCasketPhoto(usedItemName)) {
-			Common::Error lineError = playDialogueSequence(runtime,
-				kMoynahanCasketIntroLines, ARRAYSIZE(kMoynahanCasketIntroLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMoynahanCasketIntroLines,
+				ARRAYSIZE(kMoynahanCasketIntroLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -295,23 +271,22 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playDialogueSequence(runtime, kMoynahanCasketResponseOneLines,
+				lineError = runtime.playDialogueEntrySequence(kMoynahanCasketResponseOneLines,
 					ARRAYSIZE(kMoynahanCasketResponseOneLines));
 			} else if (responseIndex == 2) {
-				lineError = playMoynahanLine(0x3eb1, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x3eb1, "MOYNAHAN", 2);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			lineError = playDialogueSequence(runtime, kMoynahanCasketFollowupLines,
-				ARRAYSIZE(kMoynahanCasketFollowupLines));
+			lineError = runtime.playDialogueEntrySequence(kMoynahanCasketFollowupLines, ARRAYSIZE(kMoynahanCasketFollowupLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			if (state.gotGlue)
-				return playMoynahanLine(0x3ec3, 2);
+				return runtime.playDialogueLineWithVariant(0x3ec3, "MOYNAHAN", 2);
 
-			lineError = playMoynahanLine(0x3ec8, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x3ec8, "MOYNAHAN", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -321,7 +296,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playMoynahanLine(0x3ed2, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x3ed2, "MOYNAHAN", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -333,7 +308,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			}
 
 			if (responseIndex == 2)
-				return playMoynahanLine(0x3ed7, 1);
+				return runtime.playDialogueLineWithVariant(0x3ed7, "MOYNAHAN", 1);
 
 			return Common::kNoError;
 		}
@@ -342,20 +317,20 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				runtime.startupScript().getFlagValue("HAVE_BOTH_LEDGERS")) {
 			if (sharedState.discussedLedgerEvidence != 0 && !state.ledgerEvidenceLinePlayed) {
 				state.ledgerEvidenceLinePlayed = true;
-				Common::Error lineError = playMoynahanLine(0x3f87, 2);
+				Common::Error lineError = runtime.playDialogueLineWithVariant(0x3f87, "MOYNAHAN", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
 				runtime.setActiveSpeakerPortrait(
 					runtime.currentRoomName().equalsIgnoreCase(kMoynahanHotelRoom)
-						? kMoynahanNpc
-						: kMoynahanEmbalmingPortrait,
+						? "MOYNAHAN"
+						: "MOYNAHAN_MBLM",
 					0);
 				return Common::kNoError;
 			}
 
-			Common::Error lineError = playDialogueSequence(runtime,
-				kMoynahanLedgerIntroLines, ARRAYSIZE(kMoynahanLedgerIntroLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMoynahanLedgerIntroLines,
+				ARRAYSIZE(kMoynahanLedgerIntroLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -365,24 +340,22 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playDialogueSequence(runtime,
-					kMoynahanLedgerResponseOneLines,
+				lineError = runtime.playDialogueEntrySequence(kMoynahanLedgerResponseOneLines,
 					ARRAYSIZE(kMoynahanLedgerResponseOneLines));
 			} else if (responseIndex == 2) {
-				lineError = playDialogueSequence(runtime,
-					kMoynahanLedgerResponseTwoLines,
+				lineError = runtime.playDialogueEntrySequence(kMoynahanLedgerResponseTwoLines,
 					ARRAYSIZE(kMoynahanLedgerResponseTwoLines));
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			return playMoynahanLine(0x3ed7, 1);
+			return runtime.playDialogueLineWithVariant(0x3ed7, "MOYNAHAN", 1);
 		}
 
 		if (isBlackmailEvidence(usedItemName)) {
 			sharedState.discussedNoteCheckbookEvidence = 1;
 
-			Common::Error lineError = playMoynahanLine(0x3f3e);
+			Common::Error lineError = runtime.playDialogueLineWithVariant(0x3f3e, "MOYNAHAN", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -392,54 +365,53 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playMoynahanLine(0x3f4a);
+				lineError = runtime.playDialogueLineWithVariant(0x3f4a, "MOYNAHAN", 0);
 			} else if (responseIndex == 2) {
-				lineError = playMoynahanLine(0x3f4f, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x3f4f, "MOYNAHAN", 2);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			return playMoynahanLine(0x3f54, 2);
+			return runtime.playDialogueLineWithVariant(0x3f54, "MOYNAHAN", 2);
 		}
 
 		if (usedItemName.equalsIgnoreCase("WED_MATCHES")) {
-			Common::Error lineError = playDialogueSequence(runtime,
-				kMoynahanMatchesLines, ARRAYSIZE(kMoynahanMatchesLines));
+			Common::Error lineError = runtime.playDialogueEntrySequence(kMoynahanMatchesLines, ARRAYSIZE(kMoynahanMatchesLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			if (runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED"))
-				return playMoynahanLine(0x4054, 3);
+				return runtime.playDialogueLineWithVariant(0x4054, "MOYNAHAN", 3);
 			return Common::kNoError;
 		}
 
-		return playMoynahanLine(0x3e3f, 2);
+		return runtime.playDialogueLineWithVariant(0x3e3f, "MOYNAHAN", 2);
 	}
 
 	Common::Error lineError;
 	if (state.introPending) {
 		state.introPending = false;
-		lineError = playDialogueSequence(runtime, kMoynahanIntroLines, ARRAYSIZE(kMoynahanIntroLines));
+		lineError = runtime.playDialogueEntrySequence(kMoynahanIntroLines, ARRAYSIZE(kMoynahanIntroLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		sharedState.dialogueStateD2f04 = true;
 		const DialogueLineEntry tailLines[] = {
-			{ 0x3d50, kPcSpeaker, 0 },
-			{ 0x3d54, kMoynahanNpc, 0 }
+			{ 0x3d50, "PC", 0 },
+			{ 0x3d54, "MOYNAHAN", 0 }
 		};
-		lineError = playDialogueSequence(runtime, tailLines, ARRAYSIZE(tailLines));
+		lineError = runtime.playDialogueEntrySequence(tailLines, ARRAYSIZE(tailLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		assignTopicBuffer(runtime.currentRoomName().equalsIgnoreCase(kMoynahanEmbalmingRoom) ? 0x193 : 0x192);
+		topics.assign(runtime.currentRoomName().equalsIgnoreCase(kMoynahanEmbalmingRoom) ? 0x193 : 0x192);
 	} else {
-		lineError = playMoynahanLine(0x3fd1);
+		lineError = runtime.playDialogueLineWithVariant(0x3fd1, "MOYNAHAN", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (runtime.startupScript().getFlagValue("IF_SAW_THE_SLASHES_ON_THE_BODY") &&
 			!state.slashMarksIntroShown) {
-		lineError = playDialogueSequence(runtime, kMoynahanSlashMarksIntroLines,
+		lineError = runtime.playDialogueEntrySequence(kMoynahanSlashMarksIntroLines,
 			ARRAYSIZE(kMoynahanSlashMarksIntroLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
@@ -448,21 +420,21 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		state.slashMarksTopicUnlocked = true;
 		state.slashMarksIntroShown = true;
 		if (topicBuffer.empty())
-			assignTopicBuffer(0x194);
+			topics.assign(0x194);
 		else
 			appendTopicBuffer(0x195);
 	}
 
 	if (sharedState.discussedCasketPhotoEvidence != 0 && !state.introPending) {
 		state.introPending = true;
-		lineError = playMoynahanLine(0x3f5b, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x3f5b, "MOYNAHAN", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (runtime.startupScript().getFlagValue("HAVE_BOTH_LEDGERS") && !state.ledgersIntroduced) {
 		state.ledgersIntroduced = true;
-		lineError = playMoynahanLine(0x3f65);
+		lineError = runtime.playDialogueLineWithVariant(0x3f65, "MOYNAHAN", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -472,25 +444,25 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playMoynahanLine(0x3f70);
+			lineError = runtime.playDialogueLineWithVariant(0x3f70, "MOYNAHAN", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			(void)runtime.startupScript().resetRuntimeObjectToInitialState("LEDGER");
 			(void)runtime.startupScript().resetRuntimeObjectToInitialState("LEDGER2");
 		} else if (responseIndex == 2) {
-			lineError = playMoynahanLine(0x3f76);
+			lineError = runtime.playDialogueLineWithVariant(0x3f76, "MOYNAHAN", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			if (runtime.startupScript().getCurrentStoryDayIndex() > 2) {
 				sharedState.moynahanKarinKidnapedDiscussionState = true;
-				lineError = playMoynahanLine(0x3f7a);
+				lineError = runtime.playDialogueLineWithVariant(0x3f7a, "MOYNAHAN", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 
-			lineError = playMoynahanLine(0x3f7e, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x3f7e, "MOYNAHAN", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -499,15 +471,14 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED") &&
 			!state.stephMidgameShown) {
 		state.stephMidgameShown = true;
-		lineError = playMoynahanLine(0x3fd9);
+		lineError = runtime.playDialogueLineWithVariant(0x3fd9, "MOYNAHAN", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (runtime.startupScript().getCurrentStoryDayIndex() == 5 && !state.dayFiveShown) {
 		state.dayFiveShown = true;
-		lineError = playDialogueSequence(runtime, kMoynahanDayFiveLines,
-			ARRAYSIZE(kMoynahanDayFiveLines));
+		lineError = runtime.playDialogueEntrySequence(kMoynahanDayFiveLines, ARRAYSIZE(kMoynahanDayFiveLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -515,7 +486,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("BURNED_TV_STATION") &&
 			!state.burnedTvStationShown) {
 		state.burnedTvStationShown = true;
-		lineError = playMoynahanLine(0x3ffe);
+		lineError = runtime.playDialogueLineWithVariant(0x3ffe, "MOYNAHAN", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -523,7 +494,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("KARIN_KIDNAPED") &&
 			!state.karinKidnapedShown) {
 		state.karinKidnapedShown = true;
-		lineError = playMoynahanLine(0x4007);
+		lineError = runtime.playDialogueLineWithVariant(0x4007, "MOYNAHAN", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -533,18 +504,18 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playMoynahanLine(0x4012);
+			lineError = runtime.playDialogueLineWithVariant(0x4012, "MOYNAHAN", 0);
 		} else if (responseIndex == 2) {
 			const DialogueLineEntry responseTwoLines[] = {
-				{ 0x4017, kMoynahanNpc, 0 },
-				{ 0x401c, kPcSpeaker, 0 }
+				{ 0x4017, "MOYNAHAN", 0 },
+				{ 0x401c, "PC", 0 }
 			};
-			lineError = playDialogueSequence(runtime, responseTwoLines, ARRAYSIZE(responseTwoLines));
+			lineError = runtime.playDialogueEntrySequence(responseTwoLines, ARRAYSIZE(responseTwoLines));
 		}
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playDialogueSequence(runtime, kMoynahanKarinKidnapedTailLines,
+		lineError = runtime.playDialogueEntrySequence(kMoynahanKarinKidnapedTailLines,
 			ARRAYSIZE(kMoynahanKarinKidnapedTailLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
@@ -556,7 +527,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (!runtime.startupScript().isNamedNpcDeathTypeClear("BUTCHER") &&
 			!state.butcherDeadShown) {
 		state.butcherDeadShown = true;
-		lineError = playMoynahanLine(0x40bb, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x40bb, "MOYNAHAN", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -564,7 +535,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (!runtime.startupScript().isNamedNpcDeathTypeClear("JIMMY") &&
 			!state.jimmyDeadShown) {
 		state.jimmyDeadShown = true;
-		lineError = playMoynahanLine(0x40cd, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x40cd, "MOYNAHAN", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -572,7 +543,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("KARIN_FOUND_DEAD") &&
 			!state.karinFoundDeadShown) {
 		state.karinFoundDeadShown = true;
-		lineError = playMoynahanLine(0x40d4, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x40d4, "MOYNAHAN", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -588,37 +559,36 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (selectedTopic.equalsIgnoreCase(runtime.genericByeTopic()) ||
 				runtime.matchesResponseLine(selectedTopic, 0x199))
-			return playMoynahanLine(0x3e32, 3);
+			return runtime.playDialogueLineWithVariant(0x3e32, "MOYNAHAN", 3);
 
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMoynahanGoodCauseTopicLines,
 				ARRAYSIZE(kMoynahanGoodCauseTopicLines))) {
 			sharedState.momGoodCauseDay5State = true;
-			lineError = playDialogueSequence(runtime, kMoynahanGoodCauseLines,
-				ARRAYSIZE(kMoynahanGoodCauseLines));
+			lineError = runtime.playDialogueEntrySequence(kMoynahanGoodCauseLines, ARRAYSIZE(kMoynahanGoodCauseLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignTopicBuffer(0x19c);
+			topics.assign(0x19c);
 			continue;
 		}
 
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMoynahanSlashMarksTopicLines,
 				ARRAYSIZE(kMoynahanSlashMarksTopicLines))) {
-			lineError = playDialogueSequence(runtime, kMoynahanSlashMarksTopicReplyLines,
+			lineError = runtime.playDialogueEntrySequence(kMoynahanSlashMarksTopicReplyLines,
 				ARRAYSIZE(kMoynahanSlashMarksTopicReplyLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			assignTopicBuffer(0x1a1);
+			topics.assign(0x1a1);
 			continue;
 		}
 
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMoynahanBodyTopicLines,
 				ARRAYSIZE(kMoynahanBodyTopicLines))) {
-			lineError = playMoynahanLine(0x3da0, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x3da0, "MOYNAHAN", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			if (runtime.startupScript().getCurrentStoryDayIndex() > 4) {
-				lineError = playMoynahanLine(0x3da1, 3);
+				lineError = runtime.playDialogueLineWithVariant(0x3da1, "MOYNAHAN", 3);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -626,11 +596,11 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				state.slashMarksTopicUnlocked = true;
 			}
 
-			lineError = playMoynahanLine(0x3da2, 2);
+			lineError = runtime.playDialogueLineWithVariant(0x3da2, "MOYNAHAN", 2);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
-			assignTopicBuffer(0x1a6);
+			topics.assign(0x1a6);
 			if (runtime.startupScript().getCurrentStoryDayIndex() > 4)
 				appendTopicBuffer(0x1a7);
 			if (runtime.currentRoomName().equalsIgnoreCase(kMoynahanEmbalmingRoom))
@@ -640,7 +610,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMoynahanGeneralTopicLines,
 				ARRAYSIZE(kMoynahanGeneralTopicLines))) {
-			lineError = playMoynahanLine(0x3db1);
+			lineError = runtime.playDialogueLineWithVariant(0x3db1, "MOYNAHAN", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -650,27 +620,25 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playDialogueSequence(runtime,
-					kMoynahanGeneralTopicResponseOneLines,
+				lineError = runtime.playDialogueEntrySequence(kMoynahanGeneralTopicResponseOneLines,
 					ARRAYSIZE(kMoynahanGeneralTopicResponseOneLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignTopicBuffer(0x1ad);
+				topics.assign(0x1ad);
 			} else if (responseIndex == 2) {
-				lineError = playDialogueSequence(runtime,
-					kMoynahanGeneralTopicResponseTwoLines,
+				lineError = runtime.playDialogueEntrySequence(kMoynahanGeneralTopicResponseTwoLines,
 					ARRAYSIZE(kMoynahanGeneralTopicResponseTwoLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
 				if (runtime.startupScript().getCurrentStoryDayIndex() > 4) {
-					lineError = playMoynahanLine(0x3deb);
+					lineError = runtime.playDialogueLineWithVariant(0x3deb, "MOYNAHAN", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 					sharedState.dialogueStateD2ec8 = true;
 				}
 
-				assignTopicBuffer(0x1b0);
+				topics.assign(0x1b0);
 				if (runtime.startupScript().getCurrentStoryDayIndex() > 4)
 					appendTopicBuffer(0x1b1);
 			}
@@ -683,13 +651,12 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				sharedState.dialogueStateD2ec8 = true;
 				sharedState.dialogueStateD2ebc = true;
 				sharedState.dialogueStateD2ec0 = true;
-				lineError = playDialogueSequence(runtime, kMoynahanDayFiveUnlockLines,
-					ARRAYSIZE(kMoynahanDayFiveUnlockLines));
+				lineError = runtime.playDialogueEntrySequence(kMoynahanDayFiveUnlockLines, ARRAYSIZE(kMoynahanDayFiveUnlockLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
 				state.slashMarksTopicUnlocked = true;
-				clearTopicBuffer();
+				topics.clear();
 			}
 			continue;
 		}
@@ -697,11 +664,10 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMoynahanEmbalmingRoomTopicLines,
 				ARRAYSIZE(kMoynahanEmbalmingRoomTopicLines))) {
 			if (runtime.currentRoomName().equalsIgnoreCase(kMoynahanEmbalmingRoom)) {
-				lineError = playDialogueSequence(runtime, kMoynahanEmbalmingRoomLines,
-					ARRAYSIZE(kMoynahanEmbalmingRoomLines));
+				lineError = runtime.playDialogueEntrySequence(kMoynahanEmbalmingRoomLines, ARRAYSIZE(kMoynahanEmbalmingRoomLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				assignTopicBuffer(0x1b6);
+				topics.assign(0x1b6);
 			}
 			continue;
 		}
@@ -709,7 +675,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMoynahanLateTopicLines,
 				ARRAYSIZE(kMoynahanLateTopicLines))) {
 			if (runtime.startupScript().getCurrentStoryDayIndex() > 4) {
-				lineError = playMoynahanLine(0x40f5);
+				lineError = runtime.playDialogueLineWithVariant(0x40f5, "MOYNAHAN", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
@@ -720,7 +686,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				ARRAYSIZE(kMoynahanGlueTopicLines))) {
 			if (runtime.startupScript().getCurrentStoryDayIndex() > 4) {
 				if (!state.gotGlue) {
-					lineError = playMoynahanLine(0x40fb);
+					lineError = runtime.playDialogueLineWithVariant(0x40fb, "MOYNAHAN", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 
@@ -730,17 +696,17 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 						return responseError;
 
 					if (responseIndex == 1) {
-						lineError = playMoynahanLine(0x4108, 2);
+						lineError = runtime.playDialogueLineWithVariant(0x4108, "MOYNAHAN", 2);
 						if (lineError.getCode() != Common::kNoError)
 							return lineError;
 						queueRoomVisualMutation(giveGlue());
 					} else if (responseIndex == 2) {
-						lineError = playMoynahanLine(0x410d, 2);
+						lineError = runtime.playDialogueLineWithVariant(0x410d, "MOYNAHAN", 2);
 						if (lineError.getCode() != Common::kNoError)
 							return lineError;
 					}
 				} else {
-					lineError = playMoynahanLine(0x410d, 2);
+					lineError = runtime.playDialogueLineWithVariant(0x410d, "MOYNAHAN", 2);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				}
@@ -751,7 +717,7 @@ Common::Error MoynahanDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesResponseLine(selectedTopic, 0x1c4))
 			continue;
 
-		lineError = playMoynahanLine(0x3e39);
+		lineError = runtime.playDialogueLineWithVariant(0x3e39, "MOYNAHAN", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
diff --git a/engines/harvester/npc/moynahan_dialogue.h b/engines/harvester/npc/moynahan_dialogue.h
index f72ca8e1116..abe46548bbd 100644
--- a/engines/harvester/npc/moynahan_dialogue.h
+++ b/engines/harvester/npc/moynahan_dialogue.h
@@ -44,7 +44,6 @@ public:
 		bool karinFoundDeadShown = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override { _state = MoynahanRoomDialogueState(); }
 	void syncState(Common::Serializer &s) override {
 		syncDialogueBool(s, _state.introPending);
diff --git a/engines/harvester/npc/mr_potts_dialogue.cpp b/engines/harvester/npc/mr_potts_dialogue.cpp
index f1190d9290e..7f00aa25b5c 100644
--- a/engines/harvester/npc/mr_potts_dialogue.cpp
+++ b/engines/harvester/npc/mr_potts_dialogue.cpp
@@ -260,13 +260,6 @@ static const int kMrPottsPeepholeTopicResponseLines[] = {
 
 } // End of namespace
 
-bool MrPottsDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase("MR_POTTS") ||
-		npcName.equalsIgnoreCase("POTTS_FLESH") ||
-		npcName.equalsIgnoreCase("MR_POTTS_CEM10") ||
-		npcName.equalsIgnoreCase("MR_POTTS_HALL");
-}
-
 Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		const Common::String &usedItemName, DialogueSharedState &sharedState) {
 	MrPottsRoomDialogueState &state = _state;
@@ -279,32 +272,11 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			responseLineIndex, "Mr Potts topic buffer");
 		state.topicBufferLineIndex = mrPottsTopicBufferLineIndex;
 	};
-	auto playMrPottsLine = [&](int wavId, int headVariant) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, "MR_POTTS", headVariant);
-	};
-	auto playMrsPottsLine = [&](int wavId, int headVariant) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, "MRS_POTTS", headVariant);
-	};
-	auto playPcLine = [&](int wavId, int headVariant) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, "PC", headVariant);
-	};
-	auto playPottsFleshLine = [&](int wavId, int headVariant) -> Common::Error {
-		return runtime.playDialogueLineWithVariant(wavId, "POTTS_FLESH", headVariant);
-	};
-	auto playSequence = [&](const DialogueLineEntry *lines, uint count) -> Common::Error {
-		return runtime.playDialogueEntrySequence(lines, count);
-	};
+
 	auto hasInventoryItem = [&](const char *objectName) {
 		return runtime.startupScript().isObjectInInventory(objectName);
 	};
-	auto executeActionTagIfSet = [&](const char *tag) {
-		InteractionResult interaction;
-		if (!runtime.executeActionTag(tag, interaction))
-			return;
 
-		runtime.applyImmediateDialogueInteractionEffects(interaction);
-		runtime.queueDialogueInteractionIfNeeded(interaction);
-	};
 	auto queueNpcDeathOrMonsterfyTransition = [&](const char *npcName) {
 		InteractionResult interaction;
 		if (runtime.startupScript().finalizeRuntimeNpcDeathOrMonsterfy(npcName))
@@ -326,7 +298,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	if (hasInventoryItem("HARVEST_BLADE")) {
 		(void)runtime.startupScript().setRuntimeFlagValue("PC_TALKED_TO_POTTS", true);
-		lineError = playSequence(kMrPottsHarvestBladeOpeningLines,
+		lineError = runtime.playDialogueEntrySequence(kMrPottsHarvestBladeOpeningLines,
 			ARRAYSIZE(kMrPottsHarvestBladeOpeningLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
@@ -337,28 +309,28 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playPottsFleshLine(0x2f3d, 4);
+			lineError = runtime.playDialogueLineWithVariant(0x2f3d, "POTTS_FLESH", 4);
 		} else if (responseIndex == 2) {
-			lineError = playPottsFleshLine(0x2f42, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x2f42, "POTTS_FLESH", 0);
 		}
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		lineError = playPottsFleshLine(0x2f46, 0);
+		lineError = runtime.playDialogueLineWithVariant(0x2f46, "POTTS_FLESH", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
-		lineError = playPottsFleshLine(0x2f4d, 4);
+		lineError = runtime.playDialogueLineWithVariant(0x2f4d, "POTTS_FLESH", 4);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		executeActionTagIfSet(kMonsterfyPottsActionTag);
+		runtime.executeAndQueueAction(kMonsterfyPottsActionTag);
 		return Common::kNoError;
 	}
 
 	if (runtime.startupScript().getFlagValue("STEPHANIE_DEAD_IN_HALL_JUST_OUTSIDE")) {
 		(void)runtime.startupScript().setRuntimeFlagValue(
 			"STEPHANIE_DEAD_IN_HALL_JUST_OUTSIDE", false);
-		lineError = playSequence(kMrPottsStephanieDeadInHallLines,
+		lineError = runtime.playDialogueEntrySequence(kMrPottsStephanieDeadInHallLines,
 			ARRAYSIZE(kMrPottsStephanieDeadInHallLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
@@ -370,7 +342,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
-		executeActionTagIfSet(kStephHallExitActionTag);
+		runtime.executeAndQueueAction(kStephHallExitActionTag);
 		return Common::kNoError;
 	}
 
@@ -380,21 +352,21 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			lineError = runtime.playDialogueFst(kDialogueC016AFstPath);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playSequence(kMrPottsPottsdamDiggingIntroLines,
+			lineError = runtime.playDialogueEntrySequence(kMrPottsPottsdamDiggingIntroLines,
 				ARRAYSIZE(kMrPottsPottsdamDiggingIntroLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			return Common::kNoError;
 		}
 
-		lineError = playMrPottsLine(0x2cd9, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x2cd9, "MR_POTTS", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		return Common::kNoError;
 	}
 
 	if (runtime.startupScript().getFlagValue("POTTSDAM_DIGGING_KARIN")) {
-		lineError = playSequence(kMrPottsPottsdamDiggingKarinLines,
+		lineError = runtime.playDialogueEntrySequence(kMrPottsPottsdamDiggingKarinLines,
 			ARRAYSIZE(kMrPottsPottsdamDiggingKarinLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
@@ -405,7 +377,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	if (!usedItemName.empty()) {
 		if (usedItemName.equalsIgnoreCase("MEAT")) {
-			lineError = playMrPottsLine(0x2c7f, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x2c7f, "MR_POTTS", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -415,12 +387,12 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playMrPottsLine(0x2c8a, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x2c8a, "MR_POTTS", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				(void)runtime.startupScript().resetRuntimeObjectToInitialState(kMeatObjectName);
 			} else if (responseIndex == 2) {
-				lineError = playMrPottsLine(0x2c8f, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x2c8f, "MR_POTTS", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
@@ -429,19 +401,19 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				usedItemName.equalsIgnoreCase("CASKET_PHOTOCOPY")) {
 			sharedState.discussedCasketPhotoEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(DialogueFlags::kShownPhotoOfCorpse, true);
-			lineError = playSequence(kMrPottsCasketPhotoLines, ARRAYSIZE(kMrPottsCasketPhotoLines));
+			lineError = runtime.playDialogueEntrySequence(kMrPottsCasketPhotoLines, ARRAYSIZE(kMrPottsCasketPhotoLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			if (sharedState.dialogueStateD2ebc) {
-				lineError = playSequence(
-					kMrPottsCasketPhotoFollowupLines, ARRAYSIZE(kMrPottsCasketPhotoFollowupLines));
+				lineError = runtime.playDialogueEntrySequence(kMrPottsCasketPhotoFollowupLines,
+					ARRAYSIZE(kMrPottsCasketPhotoFollowupLines));
 			}
 		} else if (usedItemName.equalsIgnoreCase("PHOTO_OF_WHALEY_HERRILL")) {
 			sharedState.discussedWhaleyHerrillPhoto = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(
 				DialogueFlags::kShownPhotoOfWhaleyHerrill, true);
-			lineError = playSequence(
-				kMrPottsWhaleyHerrillPhotoLines, ARRAYSIZE(kMrPottsWhaleyHerrillPhotoLines));
+			lineError = runtime.playDialogueEntrySequence(kMrPottsWhaleyHerrillPhotoLines,
+				ARRAYSIZE(kMrPottsWhaleyHerrillPhotoLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -451,15 +423,15 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playMrPottsLine(0x2cca, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2cca, "MR_POTTS", 0);
 			} else if (responseIndex == 2) {
-				lineError = playMrPottsLine(0x2ccf, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2ccf, "MR_POTTS", 0);
 			} else if (responseIndex == 3) {
-				lineError = playMrPottsLine(0x2cd4, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2cd4, "MR_POTTS", 0);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playMrPottsLine(0x2cd9, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x2cd9, "MR_POTTS", 0);
 		} else if (usedItemName.equalsIgnoreCase("NOTE") ||
 				usedItemName.equalsIgnoreCase("NOTE_PHOTOCOPY") ||
 				usedItemName.equalsIgnoreCase("CHECKBOOK") ||
@@ -467,17 +439,16 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			sharedState.discussedNoteCheckbookEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(
 				DialogueFlags::kShownEvidenceOfBlackmail, true);
-			lineError = playSequence(
-				kMrPottsBlackmailEvidenceLines, ARRAYSIZE(kMrPottsBlackmailEvidenceLines));
+			lineError = runtime.playDialogueEntrySequence(kMrPottsBlackmailEvidenceLines,
+				ARRAYSIZE(kMrPottsBlackmailEvidenceLines));
 		} else if (usedItemName.equalsIgnoreCase("TV_DEED") ||
 				usedItemName.equalsIgnoreCase("TV_DEED_PHOTOCOPY")) {
 			sharedState.discussedTvDeedEvidence = 1;
 			(void)runtime.startupScript().setRuntimeFlagValue(
 				DialogueFlags::kShownEvidenceSheriffOwns, true);
-			lineError = playSequence(
-				kMrPottsTvDeedEvidenceLines, ARRAYSIZE(kMrPottsTvDeedEvidenceLines));
+			lineError = runtime.playDialogueEntrySequence(kMrPottsTvDeedEvidenceLines, ARRAYSIZE(kMrPottsTvDeedEvidenceLines));
 		} else {
-			lineError = playMrPottsLine(0x2c79, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x2c79, "MR_POTTS", 0);
 		}
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
@@ -485,7 +456,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	}
 
 	if (runtime.startupScript().getFlagValue("IF_TRY_TO_SEE_STEPHANIE")) {
-		lineError = playMrPottsLine(0x2b18, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x2b18, "MR_POTTS", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 		return Common::kNoError;
@@ -495,7 +466,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		state.auxIntroPending = false;
 		state.followupDayIndex = currentStoryDayIndex;
 
-		lineError = playMrPottsLine(0x2988, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x2988, "MR_POTTS", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -505,26 +476,26 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playSequence(kMrPottsAuxIntroResponse1Lines,
+			lineError = runtime.playDialogueEntrySequence(kMrPottsAuxIntroResponse1Lines,
 				ARRAYSIZE(kMrPottsAuxIntroResponse1Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			sharedState.dialogueStateD2f04 = true;
 			if (sharedState.dialogueStateD2ef4) {
-				lineError = playSequence(kMrPottsAuxIntroResponse1WithMomIntroLines,
+				lineError = runtime.playDialogueEntrySequence(kMrPottsAuxIntroResponse1WithMomIntroLines,
 					ARRAYSIZE(kMrPottsAuxIntroResponse1WithMomIntroLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
-			lineError = playMrPottsLine(0x29b0, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x29b0, "MR_POTTS", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			assignMrPottsTopicBuffer(
 				runtime.startupScript().getFlagValue("STEPHANIE_IS_DEAD") ? 0x1d9 : 0x1d8);
 		} else if (responseIndex == 2) {
-			lineError = playMrPottsLine(0x29ba, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x29ba, "MR_POTTS", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			assignMrPottsTopicBuffer(
@@ -532,12 +503,12 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 	} else if (currentStoryDayIndex == state.followupDayIndex) {
 		lineError = stephanieIsDead
-			? playMrPottsLine(0x2aed, 3)
-			: playMrPottsLine(0x2ac7, 1);
+			? runtime.playDialogueLineWithVariant(0x2aed, "MR_POTTS", 3)
+			: runtime.playDialogueLineWithVariant(0x2ac7, "MR_POTTS", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	} else if (!stephanieIsDead) {
-		lineError = playMrPottsLine(0x2ace, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x2ace, "MR_POTTS", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -547,11 +518,11 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playMrPottsLine(0x2ad8, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x2ad8, "MR_POTTS", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else if (responseIndex == 2) {
-			lineError = playMrPottsLine(0x2adc, 4);
+			lineError = runtime.playDialogueLineWithVariant(0x2adc, "MR_POTTS", 4);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -562,13 +533,13 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				runtime.startupScript().getFlagValue("DAY_5")) &&
 				!state.stephanieAliveLaterDayShown) {
 			state.stephanieAliveLaterDayShown = true;
-			lineError = playMrPottsLine(0x2ae5, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x2ae5, "MR_POTTS", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 		state.followupDayIndex = runtime.startupScript().getCurrentStoryDayIndex();
 	} else {
-		lineError = playMrPottsLine(0x2bdf, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x2bdf, "MR_POTTS", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -576,7 +547,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 	if (runtime.startupScript().getFlagValue("KARIN_KIDNAPED") && !state.karinKidnapedShown) {
 		state.karinKidnapedShown = true;
 
-		lineError = playMrPottsLine(0x2b1f, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x2b1f, "MR_POTTS", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -586,7 +557,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playSequence(kMrPottsKarinKidnapedResponse1Lines,
+			lineError = runtime.playDialogueEntrySequence(kMrPottsKarinKidnapedResponse1Lines,
 				ARRAYSIZE(kMrPottsKarinKidnapedResponse1Lines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -597,11 +568,11 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (nestedResponseIndex == 1) {
-				lineError = playMrPottsLine(0x2b45, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2b45, "MR_POTTS", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else if (nestedResponseIndex == 2) {
-				lineError = playMrPottsLine(0x2b4b, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2b4b, "MR_POTTS", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -610,15 +581,15 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				if (responseError.getCode() != Common::kNoError)
 					return responseError;
 
-				lineError = playSequence(kMrPottsKarinKidnapedResponse1NestedResponse2Lines,
+				lineError = runtime.playDialogueEntrySequence(kMrPottsKarinKidnapedResponse1NestedResponse2Lines,
 					ARRAYSIZE(kMrPottsKarinKidnapedResponse1NestedResponse2Lines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 		} else if (responseIndex == 2) {
 			lineError = runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED")
-				? playMrPottsLine(0x2b79, 3)
-				: playMrPottsLine(0x2b65, 0);
+				? runtime.playDialogueLineWithVariant(0x2b79, "MR_POTTS", 3)
+				: runtime.playDialogueLineWithVariant(0x2b65, "MR_POTTS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -628,22 +599,22 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			runtime.startupScript().getFlagValue("KARIN_FOUND_DEAD") &&
 			!state.karinFoundDeadShown) {
 		state.karinFoundDeadShown = true;
-		lineError = playMrPottsLine(0x2b82, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x2b82, "MR_POTTS", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (runtime.startupScript().getFlagValue("KARIN_FOUND_DEAD") && !state.karinFoundAliveShown) {
 		state.karinFoundAliveShown = true;
-		lineError = playSequence(kMrPottsKarinFoundDeadIntroLines,
+		lineError = runtime.playDialogueEntrySequence(kMrPottsKarinFoundDeadIntroLines,
 			ARRAYSIZE(kMrPottsKarinFoundDeadIntroLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
 		lineError = runtime.startupScript().getFlagValue("STEPH_MIDGAME_PLAYED")
-			? playSequence(kMrPottsKarinFoundDeadWithStephMidgameLines,
+			? runtime.playDialogueEntrySequence(kMrPottsKarinFoundDeadWithStephMidgameLines,
 				ARRAYSIZE(kMrPottsKarinFoundDeadWithStephMidgameLines))
-			: playSequence(kMrPottsKarinFoundDeadWithoutStephMidgameLines,
+			: runtime.playDialogueEntrySequence(kMrPottsKarinFoundDeadWithoutStephMidgameLines,
 				ARRAYSIZE(kMrPottsKarinFoundDeadWithoutStephMidgameLines));
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
@@ -654,11 +625,11 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playMrPottsLine(0x2bb2, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x2bb2, "MR_POTTS", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else if (responseIndex == 2) {
-			lineError = playMrPottsLine(0x2bb6, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x2bb6, "MR_POTTS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -670,7 +641,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.day6KarinShown) {
 		state.day6KarinShown = true;
 
-		lineError = playMrPottsLine(0x2bbd, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x2bbd, "MR_POTTS", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -680,11 +651,11 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playMrPottsLine(0x2bc8, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x2bc8, "MR_POTTS", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else if (responseIndex == 2) {
-			lineError = playMrPottsLine(0x2bd1, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x2bd1, "MR_POTTS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -698,12 +669,12 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 					return responseError;
 
 				if (followupResponseIndex == 1) {
-					lineError = playMrPottsLine(0x2be9, 2);
+					lineError = runtime.playDialogueLineWithVariant(0x2be9, "MR_POTTS", 2);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 
 					sharedState.discussedMrPottsTuesdayNightAlibi = true;
-					lineError = playSequence(kMrPottsDay6KarinFoundAliveAlibiFollowupLines,
+					lineError = runtime.playDialogueEntrySequence(kMrPottsDay6KarinFoundAliveAlibiFollowupLines,
 						ARRAYSIZE(kMrPottsDay6KarinFoundAliveAlibiFollowupLines));
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
@@ -719,16 +690,16 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 					if (nestedResponseIndex == 1) {
 						lineError = discussedMoynahanKarinKidnaped
-							? playMrPottsLine(0x2c15, 2)
-							: playSequence(kMrPottsDay6KarinFoundAliveNoMoynahanLines,
+							? runtime.playDialogueLineWithVariant(0x2c15, "MR_POTTS", 2)
+							: runtime.playDialogueEntrySequence(kMrPottsDay6KarinFoundAliveNoMoynahanLines,
 								ARRAYSIZE(kMrPottsDay6KarinFoundAliveNoMoynahanLines));
 					} else if (nestedResponseIndex == 2) {
-						lineError = playMrsPottsLine(0x2c24, 2);
+						lineError = runtime.playDialogueLineWithVariant(0x2c24, "MRS_POTTS", 2);
 					}
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				} else if (followupResponseIndex == 2) {
-					lineError = playMrPottsLine(0x2c2c, 0);
+					lineError = runtime.playDialogueLineWithVariant(0x2c2c, "MR_POTTS", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				}
@@ -738,34 +709,34 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 	if (runtime.startupScript().getFlagValue("SCRATCHED_TUCKER") && !state.scratchedTuckerShown) {
 		state.scratchedTuckerShown = true;
-		lineError = playMrPottsLine(0x2e54, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x2e54, "MR_POTTS", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (runtime.startupScript().getFlagValue("BARBER_POLE_STOLEN") && !state.barberPoleStolenShown) {
 		state.barberPoleStolenShown = true;
-		lineError = playMrPottsLine(0x2e61, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x2e61, "MR_POTTS", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (runtime.startupScript().getFlagValue("BOLT_OF_CLOTH_TAKEN") && !state.boltOfClothTakenShown) {
 		state.boltOfClothTakenShown = true;
-		lineError = playMrPottsLine(0x2e69, 2);
+		lineError = runtime.playDialogueLineWithVariant(0x2e69, "MR_POTTS", 2);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (runtime.startupScript().getFlagValue("DINER_BURNED") && !state.dinerBurnedShown) {
 		state.dinerBurnedShown = true;
-		lineError = playMrPottsLine(0x2e72, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x2e72, "MR_POTTS", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
 		lineError = runtime.startupScript().getFlagValue("KARIN_KIDNAPED")
-			? playMrPottsLine(0x2e77, 0)
-			: playMrPottsLine(0x2e7c, 3);
+			? runtime.playDialogueLineWithVariant(0x2e77, "MR_POTTS", 0)
+			: runtime.playDialogueLineWithVariant(0x2e7c, "MR_POTTS", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
@@ -774,7 +745,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			!state.gotRemainsForLodgeShown) {
 		state.gotRemainsForLodgeShown = true;
 
-		lineError = playMrPottsLine(0x2e82, 0);
+		lineError = runtime.playDialogueLineWithVariant(0x2e82, "MR_POTTS", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
@@ -784,49 +755,49 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			return responseError;
 
 		if (responseIndex == 1) {
-			lineError = playMrPottsLine(0x2e8d, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x2e8d, "MR_POTTS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		} else if (responseIndex == 2) {
-			lineError = playMrPottsLine(0x2e91, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x2e91, "MR_POTTS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
 
-		lineError = playMrPottsLine(0x2e95, 0);
+		lineError = runtime.playDialogueLineWithVariant(0x2e95, "MR_POTTS", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (runtime.startupScript().getFlagValue("PC_ESCAPED_JAIL") && !state.escapedJailShown) {
 		state.escapedJailShown = true;
-		lineError = playMrPottsLine(0x2e9c, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x2e9c, "MR_POTTS", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (!runtime.startupScript().isNamedNpcDeathTypeClear("BUTCHER") && !state.butcherDeathShown) {
 		state.butcherDeathShown = true;
-		lineError = playMrPottsLine(0x2ef2, 1);
+		lineError = runtime.playDialogueLineWithVariant(0x2ef2, "MR_POTTS", 1);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (!runtime.startupScript().isNamedNpcDeathTypeClear("MOYNAHAN") && !state.moynahanDeathShown) {
 		state.moynahanDeathShown = true;
-		lineError = playMrPottsLine(0x2f08, 0);
+		lineError = runtime.playDialogueLineWithVariant(0x2f08, "MR_POTTS", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
 
 	if (runtime.startupScript().getFlagValue("EDNA_HUNG") && !state.ednaHungShown) {
 		state.ednaHungShown = true;
-		lineError = playMrPottsLine(0x2f0f, 3);
+		lineError = runtime.playDialogueLineWithVariant(0x2f0f, "MR_POTTS", 3);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 
 		if (runtime.startupScript().getFlagValue("KARIN_HUNG")) {
-			lineError = playMrPottsLine(0x2f14, 3);
+			lineError = runtime.playDialogueLineWithVariant(0x2f14, "MR_POTTS", 3);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 		}
@@ -846,7 +817,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 
 		if (runtime.matchesResponseLine(selectedTopic, 0x1ea)) {
 			if (!runtime.startupScript().getFlagValue("STEPHANIE_IS_DEAD")) {
-				lineError = playSequence(kMrPottsAmnesiaTopicLines, ARRAYSIZE(kMrPottsAmnesiaTopicLines));
+				lineError = runtime.playDialogueEntrySequence(kMrPottsAmnesiaTopicLines, ARRAYSIZE(kMrPottsAmnesiaTopicLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				assignMrPottsTopicBuffer(0x1eb);
@@ -855,7 +826,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x1ec)) {
 			if (!runtime.startupScript().getFlagValue("STEPHANIE_IS_DEAD")) {
-				lineError = playSequence(kMrPottsMarriageTopicLines, ARRAYSIZE(kMrPottsMarriageTopicLines));
+				lineError = runtime.playDialogueEntrySequence(kMrPottsMarriageTopicLines, ARRAYSIZE(kMrPottsMarriageTopicLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				assignMrPottsTopicBuffer(0x1ed);
@@ -864,8 +835,8 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x1ee)) {
 			if (!runtime.startupScript().getFlagValue("STEPHANIE_IS_DEAD")) {
-				lineError = playSequence(
-					kMrPottsCommitmentTopicLines, ARRAYSIZE(kMrPottsCommitmentTopicLines));
+				lineError = runtime.playDialogueEntrySequence(kMrPottsCommitmentTopicLines,
+					ARRAYSIZE(kMrPottsCommitmentTopicLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				assignMrPottsTopicBuffer(0x1ef);
@@ -874,15 +845,15 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x1f0)) {
 			if (sharedState.dialogueStateD2ed0) {
-				lineError = playMrPottsLine(0x2a0d, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2a0d, "MR_POTTS", 0);
 			} else if (runtime.startupScript().getFlagValue("HEARD_DAD_MOAN")) {
-				lineError = playMrPottsLine(0x2a09, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2a09, "MR_POTTS", 0);
 			} else {
-				lineError = playMrPottsLine(0x2a08, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2a08, "MR_POTTS", 0);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playSequence(kMrPottsFatherTopicLines, ARRAYSIZE(kMrPottsFatherTopicLines));
+			lineError = runtime.playDialogueEntrySequence(kMrPottsFatherTopicLines, ARRAYSIZE(kMrPottsFatherTopicLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			assignMrPottsTopicBuffer(
@@ -890,7 +861,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x1f3)) {
-			lineError = playMrPottsLine(0x2a31, 1);
+			lineError = runtime.playDialogueLineWithVariant(0x2a31, "MR_POTTS", 1);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -900,13 +871,13 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playMrPottsLine(0x2a3c, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x2a3c, "MR_POTTS", 2);
 			} else if (responseIndex == 2) {
-				lineError = playMrPottsLine(0x2a42, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x2a42, "MR_POTTS", 1);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
-			lineError = playSequence(kMrPottsSlaughterhouseTopicClosingLines,
+			lineError = runtime.playDialogueEntrySequence(kMrPottsSlaughterhouseTopicClosingLines,
 				ARRAYSIZE(kMrPottsSlaughterhouseTopicClosingLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -916,7 +887,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x1f7)) {
 			if (!runtime.startupScript().getFlagValue("STEPHANIE_IS_DEAD")) {
-				lineError = playMrPottsLine(0x2a5e, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2a5e, "MR_POTTS", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				assignMrPottsTopicBuffer(0x1f8);
@@ -931,16 +902,16 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 					return responseError;
 
 				if (responseIndex == 1) {
-					lineError = playMrPottsLine(0x2a73, 0);
+					lineError = runtime.playDialogueLineWithVariant(0x2a73, "MR_POTTS", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				} else if (responseIndex == 2) {
-					lineError = playMrPottsLine(0x2a7c, 0);
+					lineError = runtime.playDialogueLineWithVariant(0x2a7c, "MR_POTTS", 0);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				}
 
-				lineError = playMrPottsLine(0x2a81, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2a81, "MR_POTTS", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				assignMrPottsTopicBuffer(0x1fb);
@@ -950,7 +921,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesResponseLine(selectedTopic, 0x1fd)) {
 			if (!runtime.startupScript().getFlagValue("STEPHANIE_IS_DEAD")) {
 				sharedState.discussedLodgeTopic = true;
-				lineError = playSequence(kMrPottsLodgeTopicLines, ARRAYSIZE(kMrPottsLodgeTopicLines));
+				lineError = runtime.playDialogueEntrySequence(kMrPottsLodgeTopicLines, ARRAYSIZE(kMrPottsLodgeTopicLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				assignMrPottsTopicBuffer(0x1fe);
@@ -958,7 +929,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x1ff)) {
-			lineError = playMrPottsLine(0x2aaf, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x2aaf, "MR_POTTS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 			assignMrPottsTopicBuffer(0x200);
@@ -967,7 +938,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMrPottsMoynahanTopicResponseLines,
 				ARRAYSIZE(kMrPottsMoynahanTopicResponseLines))) {
 			if (!runtime.startupScript().getFlagValue("STEPHANIE_IS_DEAD")) {
-				lineError = playMrPottsLine(0x2abb, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2abb, "MR_POTTS", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				assignMrPottsTopicBuffer(0x203);
@@ -979,13 +950,13 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			if (!runtime.startupScript().getFlagValue("KARIN_KIDNAPED") ||
 					runtime.startupScript().getFlagValue("KARIN_FOUND_ALIVE") ||
 					runtime.startupScript().getFlagValue("KARIN_FOUND_DEAD")) {
-				lineError = playMrPottsLine(0x2bd9, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2bd9, "MR_POTTS", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				continue;
 			}
 
-			lineError = playSequence(kMrPottsMurderAccusationOpeningLines,
+			lineError = runtime.playDialogueEntrySequence(kMrPottsMurderAccusationOpeningLines,
 				ARRAYSIZE(kMrPottsMurderAccusationOpeningLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -993,10 +964,10 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			const bool hasMoynahanAccusation = sharedState.moynahanKarinKidnapedDiscussionState;
 			const bool hasWeddingMatchesAccusation = hasInventoryItem(kWeddingMatchesObjectName);
 			if (!hasMoynahanAccusation && !hasWeddingMatchesAccusation) {
-				lineError = playPcLine(0x2e46, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x2e46, "PC", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
-				lineError = playMrPottsLine(0x2e4a, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x2e4a, "MR_POTTS", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				continue;
@@ -1027,11 +998,11 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (accusationResponseIndex == 1) {
-				lineError = playMrPottsLine(0x2d7a, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x2d7a, "MR_POTTS", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else if (accusationResponseIndex == moynahanResponseIndex) {
-				lineError = playMrPottsLine(0x2d80, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2d80, "MR_POTTS", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 
@@ -1043,11 +1014,11 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				}
 
 				if (responseIndex == 1) {
-					lineError = playMrPottsLine(0x2d8e, 2);
+					lineError = runtime.playDialogueLineWithVariant(0x2d8e, "MR_POTTS", 2);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				} else if (responseIndex == 2 && sharedState.discussedMrPottsTuesdayNightAlibi) {
-					lineError = playMrPottsLine(0x2d93, 2);
+					lineError = runtime.playDialogueLineWithVariant(0x2d93, "MR_POTTS", 2);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 					lineError = runtime.playDialogueLineWithVariant(0x2d9c, "MRS_POTTS", 1);
@@ -1055,7 +1026,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 						return lineError;
 				}
 			} else if (accusationResponseIndex == weddingMatchesResponseIndex) {
-				lineError = playSequence(kMrPottsWeddingMatchesAccusationLines,
+				lineError = runtime.playDialogueEntrySequence(kMrPottsWeddingMatchesAccusationLines,
 					ARRAYSIZE(kMrPottsWeddingMatchesAccusationLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
@@ -1063,7 +1034,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			continue;
 		}
 		if (runtime.matchesResponseLine(selectedTopic, 0x214)) {
-			lineError = playMrPottsLine(0x2dd3, 0);
+			lineError = runtime.playDialogueLineWithVariant(0x2dd3, "MR_POTTS", 0);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -1073,9 +1044,9 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playMrPottsLine(0x2ddf, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x2ddf, "MR_POTTS", 1);
 			} else if (responseIndex == 2) {
-				lineError = playMrPottsLine(0x2de4, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2de4, "MR_POTTS", 0);
 			}
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -1083,7 +1054,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		}
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMrPottsMolestTopicResponseLines,
 				ARRAYSIZE(kMrPottsMolestTopicResponseLines))) {
-			lineError = playMrPottsLine(0x2def, 4);
+			lineError = runtime.playDialogueLineWithVariant(0x2def, "MR_POTTS", 4);
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -1093,7 +1064,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playSequence(kMrPottsMolestResponse1OpeningLines,
+				lineError = runtime.playDialogueEntrySequence(kMrPottsMolestResponse1OpeningLines,
 					ARRAYSIZE(kMrPottsMolestResponse1OpeningLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
@@ -1104,17 +1075,17 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 					return responseError;
 
 				if (responseIndex == 1) {
-					lineError = playMrPottsLine(0x2e14, 3);
+					lineError = runtime.playDialogueLineWithVariant(0x2e14, "MR_POTTS", 3);
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 				} else if (responseIndex == 2) {
-					lineError = playSequence(kMrPottsMolestResponse2IntroLines,
+					lineError = runtime.playDialogueEntrySequence(kMrPottsMolestResponse2IntroLines,
 						ARRAYSIZE(kMrPottsMolestResponse2IntroLines));
 					if (lineError.getCode() != Common::kNoError)
 						return lineError;
 
 					for (;;) {
-						lineError = playMrPottsLine(0x2e2b, 3);
+						lineError = runtime.playDialogueLineWithVariant(0x2e2b, "MR_POTTS", 3);
 						if (lineError.getCode() != Common::kNoError)
 							return lineError;
 
@@ -1126,10 +1097,10 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 						if (followupResponseIndex == 1)
 							continue;
 						if (followupResponseIndex == 2) {
-							lineError = playMrPottsLine(0x2e2f, 3);
+							lineError = runtime.playDialogueLineWithVariant(0x2e2f, "MR_POTTS", 3);
 							if (lineError.getCode() != Common::kNoError)
 								return lineError;
-							lineError = playPcLine(0x2e34, 2);
+							lineError = runtime.playDialogueLineWithVariant(0x2e34, "PC", 2);
 							if (lineError.getCode() != Common::kNoError)
 								return lineError;
 						}
@@ -1137,7 +1108,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 					}
 				}
 			} else if (responseIndex == 2) {
-				lineError = playSequence(kMrPottsMolestAlternativeOpeningLines,
+				lineError = runtime.playDialogueEntrySequence(kMrPottsMolestAlternativeOpeningLines,
 					ARRAYSIZE(kMrPottsMolestAlternativeOpeningLines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
@@ -1151,14 +1122,13 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 		if (runtime.matchesAnyResponseLine(selectedTopic, kMrPottsPeepholeTopicResponseLines,
 				ARRAYSIZE(kMrPottsPeepholeTopicResponseLines))) {
 			if (runtime.startupScript().getFlagValue("STEPHANIE_IS_DEAD")) {
-				lineError = playMrPottsLine(0x2bd9, 0);
+				lineError = runtime.playDialogueLineWithVariant(0x2bd9, "MR_POTTS", 0);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 				continue;
 			}
 
-			lineError = playSequence(kMrPottsPeepholeOpeningLines,
-				ARRAYSIZE(kMrPottsPeepholeOpeningLines));
+			lineError = runtime.playDialogueEntrySequence(kMrPottsPeepholeOpeningLines, ARRAYSIZE(kMrPottsPeepholeOpeningLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
@@ -1168,27 +1138,26 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 				return responseError;
 
 			if (responseIndex == 1) {
-				lineError = playMrPottsLine(0x2d14, 2);
+				lineError = runtime.playDialogueLineWithVariant(0x2d14, "MR_POTTS", 2);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else if (responseIndex == 2) {
-				lineError = playSequence(kMrPottsPeepholeResponse2Lines,
+				lineError = runtime.playDialogueEntrySequence(kMrPottsPeepholeResponse2Lines,
 					ARRAYSIZE(kMrPottsPeepholeResponse2Lines));
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			} else if (responseIndex == 3) {
-				lineError = playMrPottsLine(0x2d28, 1);
+				lineError = runtime.playDialogueLineWithVariant(0x2d28, "MR_POTTS", 1);
 				if (lineError.getCode() != Common::kNoError)
 					return lineError;
 			}
 
-			lineError = playSequence(kMrPottsPeepholeClosingLines,
-				ARRAYSIZE(kMrPottsPeepholeClosingLines));
+			lineError = runtime.playDialogueEntrySequence(kMrPottsPeepholeClosingLines, ARRAYSIZE(kMrPottsPeepholeClosingLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
 
 			sharedState.confrontedMrPottsAboutSpyhole = true;
-			lineError = playSequence(kMrPottsPeepholeFollowupLines,
+			lineError = runtime.playDialogueEntrySequence(kMrPottsPeepholeFollowupLines,
 				ARRAYSIZE(kMrPottsPeepholeFollowupLines));
 			if (lineError.getCode() != Common::kNoError)
 				return lineError;
@@ -1197,7 +1166,7 @@ Common::Error MrPottsDialogueHandler::handleDialogue(DialogueRuntime &runtime,
 			continue;
 		}
 
-		lineError = playMrPottsLine(0x2c73, 0);
+		lineError = runtime.playDialogueLineWithVariant(0x2c73, "MR_POTTS", 0);
 		if (lineError.getCode() != Common::kNoError)
 			return lineError;
 	}
diff --git a/engines/harvester/npc/mr_potts_dialogue.h b/engines/harvester/npc/mr_potts_dialogue.h
index f2727b9faa9..fe48f4183d9 100644
--- a/engines/harvester/npc/mr_potts_dialogue.h
+++ b/engines/harvester/npc/mr_potts_dialogue.h
@@ -50,7 +50,6 @@ public:
 		bool day6KarinFoundAliveFollowupShown = false;
 	};
 
-	bool matchesNpc(const Common::String &npcName) const override;
 	void resetState() override {
 		const int topicBufferLineIndex = _state.topicBufferLineIndex;
 		_state = MrPottsRoomDialogueState();
diff --git a/engines/harvester/npc/mrs_potts_dialogue.cpp b/engines/harvester/npc/mrs_potts_dialogue.cpp
index a7e0463006a..cfde17bcb02 100644
--- a/engines/harvester/npc/mrs_potts_dialogue.cpp
+++ b/engines/harvester/npc/mrs_potts_dialogue.cpp
@@ -132,35 +132,14 @@ static const int kMrsPottsLodgeTopicResponseLines[] = { 0x247, 0x248 };
 
 } // End of namespace
 
-bool MrsPottsDialogueHandler::matchesNpc(const Common::String &npcName) const {
-	return npcName.equalsIgnoreCase("MRS_POTTS") ||
-		npcName.equalsIgnoreCase("MRS_POTTS_ST_BEDRM");


Commit: 8ae6a2999b4614a8960112092c037efe0d7b7a65
    https://github.com/scummvm/scummvm/commit/8ae6a2999b4614a8960112092c037efe0d7b7a65
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:06-04:00

Commit Message:
HARVESTER: refactor player state management

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/combat.cpp
    engines/harvester/combat.h
    engines/harvester/flow.cpp
    engines/harvester/input.cpp
    engines/harvester/interaction.cpp
    engines/harvester/interaction.h
    engines/harvester/player.cpp
    engines/harvester/player.h
    engines/harvester/room_support.h
    engines/harvester/scene.cpp
    engines/harvester/scene.h


diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat.cpp
index c3b20c1ad67..ee6e1325fa4 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -737,10 +737,10 @@ MonsterRecord *Combat::findProjectileContactMonsterTarget(RoomSetupState &state,
 	return bestMonster;
 }
 
-void Combat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, RoomPlayerState &playerState,
+void Combat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, Player &player,
 		EntityManager *entityManager, bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const {
-	playerState.attackTargetName.clear();
-	playerState.attackTargetClassId = -1;
+	const RoomPlayerState &playerState = player.getState();
+	player.clearAttackTarget();
 	if (!playerState.entity)
 		return;
 
@@ -838,8 +838,7 @@ void Combat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, RoomPla
 		return;
 	}
 
-	playerState.attackTargetName = bestName;
-	playerState.attackTargetClassId = bestClassId;
+	player.captureAttackTarget(bestName, bestClassId);
 	debugC(1, kDebugCombat,
 		"Harvester: combat player keyboard target capture class='%s' name='%s' input=(L=%d R=%d U=%d D=%d) facing=%d target_rect=(%d,%d)-(%d,%d)",
 		describeCombatTargetClass(bestClassId), bestName.c_str(),
@@ -902,11 +901,11 @@ MonsterRecord *Combat::resolveKillTarget(RoomSetupState &state, const RoomPlayer
 	return bestMonster;
 }
 
-void Combat::capturePlayerAttackTarget(RoomSetupState &state, RoomPlayerState &playerState,
+void Combat::capturePlayerAttackTarget(RoomSetupState &state, Player &player,
 		EntityManager *entityManager, const Common::Point &mousePos, const NpcRecord *hoverNpc) const {
+	const RoomPlayerState &playerState = player.getState();
 	if (hoverNpc) {
-		playerState.attackTargetName = hoverNpc->npcName;
-		playerState.attackTargetClassId = kEntityClassNpc;
+		player.captureAttackTarget(hoverNpc->npcName, kEntityClassNpc);
 		debugC(1, kDebugCombat,
 			"Harvester: combat player target capture class='npc' name='%s' cursor=(%d,%d)",
 			playerState.attackTargetName.c_str(), mousePos.x, mousePos.y);
@@ -914,8 +913,7 @@ void Combat::capturePlayerAttackTarget(RoomSetupState &state, RoomPlayerState &p
 	}
 
 	if (MonsterRecord *monster = findMonsterTargetAtPoint(state, entityManager, mousePos)) {
-		playerState.attackTargetName = monster->monsterName;
-		playerState.attackTargetClassId = kEntityClassMonster;
+		player.captureAttackTarget(monster->monsterName, kEntityClassMonster);
 		debugC(1, kDebugCombat,
 			"Harvester: combat player target capture class='monster' name='%s' cursor=(%d,%d)",
 			playerState.attackTargetName.c_str(), mousePos.x, mousePos.y);
@@ -927,11 +925,11 @@ void Combat::capturePlayerAttackTarget(RoomSetupState &state, RoomPlayerState &p
 		mousePos.x, mousePos.y);
 }
 
-void Combat::captureOverlappingNpcAttackTarget(RoomSetupState &state, RoomPlayerState &playerState, EntityManager *entityManager) const {
+void Combat::captureOverlappingNpcAttackTarget(RoomSetupState &state, Player &player, EntityManager *entityManager) const {
+	const RoomPlayerState &playerState = player.getState();
 	if (playerState.attackTargetClassId < 0 && !Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
 		if (NpcRecord *fallbackNpc = findOverlappingNpcTarget(state, playerState, entityManager)) {
-			playerState.attackTargetName = fallbackNpc->npcName;
-			playerState.attackTargetClassId = kEntityClassNpc;
+			player.captureAttackTarget(fallbackNpc->npcName, kEntityClassNpc);
 		}
 	}
 }
@@ -946,8 +944,9 @@ NpcRecord *Combat::selectNpcAttackContactTarget(RoomSetupState &state, const Roo
 	return npc;
 }
 
-MonsterRecord *Combat::selectMonsterAttackContactTarget(RoomSetupState &state, RoomPlayerState &playerState,
+MonsterRecord *Combat::selectMonsterAttackContactTarget(RoomSetupState &state, Player &player,
 		EntityManager *entityManager, Entity *&monsterEntity) const {
+	const RoomPlayerState &playerState = player.getState();
 	MonsterRecord *monster = nullptr;
 	if (playerState.attackTargetClassId == kEntityClassMonster)
 		monster = findRoomMonsterRecordByName(state, playerState.attackTargetName);
@@ -958,8 +957,7 @@ MonsterRecord *Combat::selectMonsterAttackContactTarget(RoomSetupState &state, R
 		} else if (playerState.attackTargetClassId < 0) {
 			monster = findProjectileContactMonsterTarget(state, playerState, entityManager);
 			if (monster) {
-				playerState.attackTargetName = monster->monsterName;
-				playerState.attackTargetClassId = kEntityClassMonster;
+				player.captureAttackTarget(monster->monsterName, kEntityClassMonster);
 			}
 		} else {
 			monster = nullptr;
@@ -1173,8 +1171,9 @@ void Combat::startMonsterHitReaction(HarvesterEngine &engine,
 }
 
 Common::Error Combat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupState &state,
-		RoomPlayerState &playerState, EntityManager *entityManager, const Common::Point &mousePos,
+		Player &player, EntityManager *entityManager, const Common::Point &mousePos,
 		CombatEffects &combatEffects, Interaction &interactionProcessor, bool &needsRedraw) {
+	const RoomPlayerState &playerState = player.getState();
 	if (!engine.isCombatDebugEnabled())
 		return Common::kNoError;
 	MonsterRecord *monster = resolveKillTarget(state, playerState, entityManager, mousePos);
@@ -1202,8 +1201,7 @@ Common::Error Combat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupS
 
 	if (playerState.attackTargetClassId == kEntityClassMonster &&
 			playerState.attackTargetName.equalsIgnoreCase(monster->monsterName)) {
-		playerState.attackTargetName.clear();
-		playerState.attackTargetClassId = -1;
+		player.clearAttackTarget();
 	}
 
 	debugC(1, kDebugCombat,
@@ -1256,9 +1254,10 @@ Common::Error Combat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupS
 }
 
 Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
-		RoomPlayerState &playerState, RoomIdleAnimationState &idleState, EntityManager *entityManager,
+		Player &player, EntityManager *entityManager,
 		SceneEntities &sceneRuntime, CombatEffects &combatEffects, const CheatSystem &cheats,
 		InteractionCallbacks &callbacks, Interaction &interactionProcessor, bool &needsRedraw) {
+	const RoomPlayerState &playerState = player.getState();
 	if (!entityManager)
 		return Common::kNoError;
 
@@ -1413,17 +1412,9 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 						damageDisabled ? 1 : 0);
 					if (damageLanded > 0 && playerState.entity) {
 						combatEffects.spawnCombatDamagePopup(*playerState.entity, playerState.entity->getName(), damageLanded);
-						if (idleState.entity) {
-							idleState.entity->setVisible(false);
-							idleState.entity->setAnimationRate(0);
-						}
-						idleState.active = false;
-						idleState.loopStarted = false;
-						idleState.exiting = false;
-						idleState.restoreFacing = -1;
-						playerState.entity->setVisible(true);
+						player.cancelIdleAnimation();
 						if (playerHitPointsAfter > 0 &&
-								Player::startHitAnimation(engine, playerState, combatState.attackFirstFrame)) {
+								player.startHitAnimation(engine, combatState.attackFirstFrame)) {
 							callbacks.notePlayerActivity();
 						}
 					}
@@ -1583,17 +1574,18 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 }
 
 Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
-		RoomSetupState &state, RoomPlayerState &playerState,
+		RoomSetupState &state, Player &player,
 		EntityManager *entityManager, SceneEntities &sceneRuntime,
 		CombatEffects &combatEffects,
 		Interaction &interactionProcessor, bool &needsRedraw) {
+	const RoomPlayerState &playerState = player.getState();
 	if (!playerState.attackActive || playerState.attackContactResolved || !playerState.entity ||
 			playerState.attackContactFrame < 0 ||
 			playerState.entity->getCurrentFrame() < playerState.attackContactFrame) {
 		return Common::kNoError;
 	}
 
-	playerState.attackContactResolved = true;
+	player.markAttackContactResolved();
 	const int damageType = Player::resolveCombatLoadoutDamageType(playerState.combatLoadout);
 	const int damageAmount = Player::resolveCombatLoadoutDamageAmount(playerState.combatLoadout);
 	debugC(1, kDebugCombat,
@@ -1606,7 +1598,7 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 	if (!script)
 		return Common::kNoError;
 
-	captureOverlappingNpcAttackTarget(state, playerState, entityManager);
+	captureOverlappingNpcAttackTarget(state, player, entityManager);
 	if (playerState.attackTargetClassId == kEntityClassNpc) {
 		Entity *npcEntity = nullptr;
 		NpcRecord *npc = selectNpcAttackContactTarget(state, playerState, entityManager, npcEntity);
@@ -1658,7 +1650,7 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 	}
 
 	Entity *monsterEntity = nullptr;
-	MonsterRecord *monster = selectMonsterAttackContactTarget(state, playerState, entityManager, monsterEntity);
+	MonsterRecord *monster = selectMonsterAttackContactTarget(state, player, entityManager, monsterEntity);
 	if (!monster || !monsterEntity) {
 		debugC(1, kDebugCombat,
 			"Harvester: combat player attack missed class='monster' target='%s' reason='no reachable monster'",
diff --git a/engines/harvester/combat.h b/engines/harvester/combat.h
index cb04d5f37ea..4d8e9d2d8df 100644
--- a/engines/harvester/combat.h
+++ b/engines/harvester/combat.h
@@ -35,6 +35,8 @@ class CombatEffects;
 class InteractionCallbacks;
 class Interaction;
 class SceneEntities;
+class Player;
+struct RoomPlayerState;
 
 extern const int kRoomMonsterAnimationRate;
 extern const int kNativeMonsterAttackAnimationRate;
@@ -137,29 +139,29 @@ public:
 	void commitMonsterCombatStatesForSceneChange(Common::Array<MonsterCombatState> &&states);
 	void resetForSceneRefresh(const RoomSetupState &state);
 
-	void capturePlayerAttackTargetForKeyboard(RoomSetupState &state, RoomPlayerState &playerState,
+	void capturePlayerAttackTargetForKeyboard(RoomSetupState &state, Player &player,
 		EntityManager *entityManager, bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const;
 	MonsterRecord *resolveKillTarget(RoomSetupState &state, const RoomPlayerState &playerState,
 		EntityManager *entityManager, const Common::Point &mousePos) const;
 	// The caller clears the captured target before resolving the room hover state.
-	void capturePlayerAttackTarget(RoomSetupState &state, RoomPlayerState &playerState,
+	void capturePlayerAttackTarget(RoomSetupState &state, Player &player,
 		EntityManager *entityManager, const Common::Point &mousePos, const NpcRecord *hoverNpc) const;
-	void captureOverlappingNpcAttackTarget(RoomSetupState &state, RoomPlayerState &playerState, EntityManager *entityManager) const;
+	void captureOverlappingNpcAttackTarget(RoomSetupState &state, Player &player, EntityManager *entityManager) const;
 	NpcRecord *selectNpcAttackContactTarget(RoomSetupState &state, const RoomPlayerState &playerState,
 		EntityManager *entityManager, Entity *&npcEntity) const;
-	MonsterRecord *selectMonsterAttackContactTarget(RoomSetupState &state, RoomPlayerState &playerState,
+	MonsterRecord *selectMonsterAttackContactTarget(RoomSetupState &state, Player &player,
 		EntityManager *entityManager, Entity *&monsterEntity) const;
 
 	Common::Error killDebugActiveMonster(HarvesterEngine &engine, RoomSetupState &state,
-		RoomPlayerState &playerState, EntityManager *entityManager, const Common::Point &mousePos,
+		Player &player, EntityManager *entityManager, const Common::Point &mousePos,
 		CombatEffects &combatEffects, Interaction &interactionProcessor, bool &needsRedraw);
 
 	Common::Error updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
-		RoomPlayerState &playerState, RoomIdleAnimationState &idleState, EntityManager *entityManager,
+		Player &player, EntityManager *entityManager,
 		SceneEntities &sceneRuntime, CombatEffects &combatEffects, const CheatSystem &cheats,
 		InteractionCallbacks &callbacks, Interaction &interactionProcessor, bool &needsRedraw);
 	Common::Error resolvePlayerAttackContact(HarvesterEngine &engine,
-		RoomSetupState &state, RoomPlayerState &playerState,
+		RoomSetupState &state, Player &player,
 		EntityManager *entityManager, SceneEntities &sceneRuntime,
 		CombatEffects &combatEffects,
 		Interaction &interactionProcessor, bool &needsRedraw);
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 21bc79e07d5..83844608f46 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -1257,22 +1257,17 @@ void logStartupRoomProbe(HarvesterEngine &engine, const SceneState &scene,
 
 			Entity *player = entityManager->findSceneEntityByName(kPlayerActorEntityName);
 			if (player && foundFloorProbe && floorHover.cursorSequence == kCursorSequenceWalk) {
-				RoomPlayerState probePlayer;
-				probePlayer.entity = player;
-				probePlayer.centerX = scene.state.playerSpawnX;
-				probePlayer.bottomY = scene.state.playerSpawnY;
-				probePlayer.z = (float)scene.state.playerSpawnZ;
-				probePlayer.facing = scene.state.playerFacing;
-				Player::setMoveTargetFromScreenPoint(scene.state, probePlayer, floorProbe.x, floorProbe.y);
-				for (int i = 0; i < 32 && probePlayer.hasMoveTarget; ++i)
-					(void)Player::stepMoveTarget(
-						engine, scene.state, scene.sceneObjects, scene.sceneAnimations, probePlayer);
+				Player probePlayer;
+				probePlayer.initialize(scene.state, player, 0);
+				probePlayer.setMoveTargetFromScreenPoint(scene.state, floorProbe.x, floorProbe.y);
+				for (int i = 0; i < 32 && probePlayer.getState().hasMoveTarget; ++i)
+					(void)probePlayer.stepMoveTarget(engine, scene.state, scene.sceneObjects, scene.sceneAnimations);
 
 				const Common::Rect movedRect = player->getScreenRect();
 				debugC(1, kDebugPlayer,
 					"Harvester: startup probe movement room='%s' target=(%d,%d) final_rect=(%d,%d)-(%d,%d) z=%.2f",
 					scene.state.roomName.c_str(), floorProbe.x, floorProbe.y,
-					movedRect.left, movedRect.top, movedRect.right, movedRect.bottom, (double)probePlayer.z);
+					movedRect.left, movedRect.top, movedRect.right, movedRect.bottom, (double)probePlayer.getState().z);
 
 				player->setCurrentFrame(Player::resolveFacingFrame(scene.state.playerFacing));
 				(void)applyRoomActorPlacement(scene.state, *player,
@@ -2180,26 +2175,16 @@ bool Flow::populateRoomSceneEntities(RoomSetupState &state,
 					state.entranceName.c_str());
 			}
 			if (reusedPlayer) {
-				RoomPlayerState playerState;
-				playerState.entity = player;
-				playerState.centerX = state.playerSpawnX;
-				playerState.bottomY = state.playerSpawnY;
-				playerState.z = (float)state.playerSpawnZ;
-				playerState.facing = state.playerFacing;
-				playerState.combatLoadout = -1;
-				(void)Player::syncCombatLoadoutVisual(_engine, state, playerState, playerCombatLoadout);
+				Player loadoutPlayer;
+				loadoutPlayer.initialize(state, player, -1);
+				(void)loadoutPlayer.syncCombatLoadoutVisual(_engine, state, playerCombatLoadout);
 			}
-			RoomPlayerState playerSpawnState;
-			playerSpawnState.entity = player;
-			playerSpawnState.centerX = state.playerSpawnX;
-			playerSpawnState.bottomY = state.playerSpawnY;
-			playerSpawnState.z = (float)state.playerSpawnZ;
-			playerSpawnState.facing = state.playerFacing;
-			if (Player::resolveBlockedStartupSpawn(
-					_engine, state, drawableObjects, drawableAnimations, playerSpawnState)) {
-				state.playerSpawnX = playerSpawnState.centerX;
-				state.playerSpawnY = playerSpawnState.bottomY;
-				state.playerSpawnZ = (int)playerSpawnState.z;
+			Player spawnPlayer;
+			spawnPlayer.initialize(state, player, 0);
+			if (spawnPlayer.resolveBlockedStartupSpawn(_engine, state, drawableObjects, drawableAnimations)) {
+				state.playerSpawnX = spawnPlayer.getState().centerX;
+				state.playerSpawnY = spawnPlayer.getState().bottomY;
+				state.playerSpawnZ = (int)spawnPlayer.getState().z;
 			}
 			if (reusedPlayer)
 				entityManager->adoptSceneEntity(player);
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index 28aebb316b5..d4db61fbeb5 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -114,7 +114,7 @@ void Input::queueCombatLoadoutStatusMessage(const Common::String &message, bool
 }
 
 bool Input::openInventoryOverlay() {
-	RoomPlayerState &playerState = _session.getPlayerState();
+	Player &player = _session.getPlayer();
 	InputState &inputState = _session.getInputState();
 
 	inputState.moveLeft = false;
@@ -123,19 +123,16 @@ bool Input::openInventoryOverlay() {
 	inputState.moveDown = false;
 	_session.clearPendingRegionInteraction();
 	(void)_session.cancelPlayerAttackAnimation();
-	playerState.hasMoveTarget = false;
-	playerState.turnActive = false;
-	playerState.turnTargetFacing = -1;
+	player.cancelMovement();
 	if (!_roomItems.stowCarriedRoomItemToInventory())
 		return false;
 	return _inventory.open();
 }
 
 void Input::capturePlayerAttackTarget() {
-	RoomPlayerState &playerState = _session.getPlayerState();
+	Player &player = _session.getPlayer();
 
-	playerState.attackTargetName.clear();
-	playerState.attackTargetClassId = -1;
+	player.clearAttackTarget();
 
 	const RoomHoverState hoverState = resolveRoomHoverState(
 		_engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs, _scene.sceneRegions,
@@ -157,12 +154,13 @@ void Input::handleMouseMove(bool &needsRedraw) {
 }
 
 InputResult Input::handleRightButtonDown(bool &needsRedraw) {
-	RoomPlayerState &playerState = _session.getPlayerState();
-	RoomIdleAnimationState &idleState = _session.getIdleState();
+	Player &player = _session.getPlayer();
+	const RoomPlayerState &playerState = player.getState();
+	const RoomIdleAnimationState &idleState = player.getIdleState();
 
 	_session.notePlayerActivity();
 	if (idleState.active || idleState.exiting) {
-		if (Player::requestIdleAnimationExit(_scene.state, playerState, idleState))
+		if (player.requestIdleAnimationExit(_scene.state))
 			needsRedraw = true;
 		return InputResult();
 	}
@@ -202,13 +200,7 @@ InputResult Input::handleRightButtonDown(bool &needsRedraw) {
 						_presentationState.closeInventoryAfterCombatLoadoutStatus = true;
 					}
 					_session.captureCurrentSaveState();
-					if (playerState.entity) {
-						(void)Player::syncCombatLoadoutVisual(_engine, _scene.state, playerState,
-							_engine.getScript()->getPlayerCombatLoadout());
-					} else if (_engine.getScript()) {
-						playerState.combatLoadout =
-							_engine.getScript()->getPlayerCombatLoadout();
-					}
+					player.syncCombatLoadout(_engine, _scene.state);
 					if (!_inventory.refresh())
 						return InputResult::leaveRoom(Common::kReadingFailed);
 				}
@@ -282,17 +274,10 @@ InputResult Input::handleRightButtonDown(bool &needsRedraw) {
 	Script *script = _engine.getScript();
 	if (!_presentationState.showingInspectText && !_session.isPlayerCombatLocked() && script && playerState.entity &&
 			script->getPlayerCurrentHitPoints() > 0 && !script->isPlayerControlPaused()) {
-		playerState.hasMoveTarget = false;
-		playerState.turnActive = false;
-		playerState.turnTargetFacing = -1;
-		playerState.turnFirstFrame = -1;
-		playerState.turnLastFrame = -1;
-		playerState.turnEndFrame = -1;
-		playerState.turnPlayBackwards = false;
-		(void)Player::syncCombatLoadoutVisual(_engine, _scene.state, playerState,
-			script->getPlayerCombatLoadout());
+		player.cancelMovementForAttack();
+		(void)player.syncCombatLoadoutVisual(_engine, _scene.state, script->getPlayerCombatLoadout());
 		capturePlayerAttackTarget();
-		if (Player::startAttackAnimation(_scene.state, playerState, _flow._mousePos)) {
+		if (player.startAttackAnimation(_scene.state, _flow._mousePos)) {
 			debugC(1, kDebugCombat,
 				"Harvester: combat player attack start loadout=%d weapon='%s' damage=%d damage_type='%s' target_class='%s' target='%s' cursor=(%d,%d)",
 				playerState.combatLoadout,
@@ -303,8 +288,7 @@ InputResult Input::handleRightButtonDown(bool &needsRedraw) {
 				playerState.attackTargetName.c_str(), _flow._mousePos.x, _flow._mousePos.y);
 			needsRedraw = true;
 		} else {
-			playerState.attackTargetName.clear();
-			playerState.attackTargetClassId = -1;
+			player.clearAttackTarget();
 		}
 	}
 	return InputResult();
@@ -319,8 +303,9 @@ void Input::handleLeftButtonUp() {
 }
 
 InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
-	RoomPlayerState &playerState = _session.getPlayerState();
-	RoomIdleAnimationState &idleState = _session.getIdleState();
+	Player &player = _session.getPlayer();
+	const RoomPlayerState &playerState = player.getState();
+	const RoomIdleAnimationState &idleState = player.getIdleState();
 	InputState &inputState = _session.getInputState();
 
 	const uint32 now = Player::getRuntimeClockTicks();
@@ -329,7 +314,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 		now - inputState.lastLeftButtonReleaseTick < kRoomExitFastClickWindowTicks;
 	_session.notePlayerActivity();
 	if (idleState.active || idleState.exiting) {
-		if (Player::requestIdleAnimationExit(_scene.state, playerState, idleState))
+		if (player.requestIdleAnimationExit(_scene.state))
 			needsRedraw = true;
 		return InputResult();
 	}
@@ -486,9 +471,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 		inputState.moveRight = false;
 		inputState.moveUp = false;
 		inputState.moveDown = false;
-		playerState.hasMoveTarget = false;
-		playerState.turnActive = false;
-		playerState.turnTargetFacing = -1;
+		player.cancelMovement();
 		bool didTransition = false;
 		Common::Error dialogueError = _interactionProcessor.runScriptedDialogue(
 			hoverState.npc->npcName, Common::String(), Common::String(), didTransition);
@@ -538,7 +521,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 					playerState.centerX, playerState.bottomY, (double)playerState.z,
 					playerRect.left, playerRect.top, playerRect.right, playerRect.bottom);
 			}
-			Player::setMoveTargetFromScreenPoint(_scene.state, playerState, _flow._mousePos.x, _flow._mousePos.y);
+			player.setMoveTargetFromScreenPoint(_scene.state, _flow._mousePos.x, _flow._mousePos.y);
 			if (_engine.isPathfindingDebugEnabled()) {
 				debugC(1, kDebugPathfinding,
 					"Harvester: pathfinding click target room='%s' mouse=(%d,%d) target=(%d,%d,z=%.2f)",
@@ -577,9 +560,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 			debug(1, "Harvester: unsupported IDENT textbox '%s' for object '%s'",
 				resolvedInspectText.boxName.c_str(), clickedObject->objectName.c_str());
 		}
-		playerState.hasMoveTarget = false;
-		playerState.turnActive = false;
-		playerState.turnTargetFacing = -1;
+		player.cancelMovement();
 		needsRedraw = true;
 		return InputResult();
 	}
@@ -617,8 +598,8 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 }
 
 InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw) {
-	RoomPlayerState &playerState = _session.getPlayerState();
-	RoomIdleAnimationState &idleState = _session.getIdleState();
+	Player &player = _session.getPlayer();
+	const RoomIdleAnimationState &idleState = player.getIdleState();
 	InputState &inputState = _session.getInputState();
 
 	if (_presentationState.showingInspectText) {
@@ -635,7 +616,7 @@ InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw)
 	if (key.hasFlags(Common::KBD_CTRL))
 		inputState.attackModifierHeld = true;
 	if (idleState.active || idleState.exiting) {
-		if (Player::requestIdleAnimationExit(_scene.state, playerState, idleState))
+		if (player.requestIdleAnimationExit(_scene.state))
 			needsRedraw = true;
 		return InputResult();
 	}
@@ -734,9 +715,7 @@ InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw)
 		inputState.moveUp = false;
 		inputState.moveDown = false;
 		_session.clearPendingRegionInteraction();
-		playerState.hasMoveTarget = false;
-		playerState.turnActive = false;
-		playerState.turnTargetFacing = -1;
+		player.cancelMovement();
 		_session.captureCurrentSaveState();
 		Graphics::Screen *activeScreen = _engine.getScreen();
 		if (!activeScreen)
diff --git a/engines/harvester/interaction.cpp b/engines/harvester/interaction.cpp
index 77d949f5cbc..2cc52ad8511 100644
--- a/engines/harvester/interaction.cpp
+++ b/engines/harvester/interaction.cpp
@@ -348,12 +348,12 @@ Common::Error ItemInteractions::handleRoomItemPickup(const ObjectRecord &object,
 }
 
 Interaction::Interaction(HarvesterEngine &engine, Flow &flow,
-		SceneState &scene, RoomPlayerState &playerState,
+		SceneState &scene, Player &player,
 		Common::String &pendingRegionName, Common::String &pendingRoomChange,
 		bool &pendingRoomChangeIsRoomName, bool &pendingRoomChangeUsesSavedRoomState,
 		SaveRoomState &pendingRoomChangeSavedRoomState, bool canExitCloseupToParent,
 		InteractionCallbacks &callbacks)
-	: _engine(engine), _flow(flow), _scene(scene), _playerState(playerState),
+	: _engine(engine), _flow(flow), _scene(scene), _player(player),
 	  _pendingRegionName(pendingRegionName), _pendingRoomChange(pendingRoomChange),
 	  _pendingRoomChangeIsRoomName(pendingRoomChangeIsRoomName),
 	  _pendingRoomChangeUsesSavedRoomState(pendingRoomChangeUsesSavedRoomState),
@@ -362,13 +362,14 @@ Interaction::Interaction(HarvesterEngine &engine, Flow &flow,
 }
 
 void Interaction::queueRegionInteraction(const RegionRecord &region, const Common::Point &mousePos) {
+	const RoomPlayerState &playerState = _player.getState();
 	_pendingRegionName = region.regionName;
-	if (!_playerState.entity)
+	if (!playerState.entity)
 		return;
 
-	const bool playerOverlapsRegion = doesPlayerOverlapRegion(*_playerState.entity, region);
+	const bool playerOverlapsRegion = doesPlayerOverlapRegion(*playerState.entity, region);
 	if (_engine.isPathfindingDebugEnabled()) {
-		const Common::Rect playerRect = _playerState.entity->getScreenRect();
+		const Common::Rect playerRect = playerState.entity->getScreenRect();
 		const Common::Rect regionBounds(region.left, region.top, region.right + 1, region.bottom + 1);
 		debugC(1, kDebugPathfinding,
 			"Harvester: pathfinding region click room='%s' mouse=(%d,%d) region='%s' bounds=(%d,%d)-(%d,%d) z=[%d,%d] start=%d cursor=%d desired_facing=%d player=(%d,%d,z=%.2f) player_rect=(%d,%d)-(%d,%d) overlaps=%d action='%s'",
@@ -377,33 +378,33 @@ void Interaction::queueRegionInteraction(const RegionRecord &region, const Commo
 			regionBounds.left, regionBounds.top, regionBounds.right, regionBounds.bottom,
 			region.minZ, region.maxZ, region.startEnabled, region.cursorEnabled,
 			region.desiredFacing,
-			_playerState.centerX, _playerState.bottomY, (double)_playerState.z,
+			playerState.centerX, playerState.bottomY, (double)playerState.z,
 			playerRect.left, playerRect.top, playerRect.right, playerRect.bottom,
 			playerOverlapsRegion, region.actionTag.c_str());
 	}
-	if (playerOverlapsRegion && doesPlayerFacingMatchRegion(_playerState.facing, region))
+	if (playerOverlapsRegion && doesPlayerFacingMatchRegion(playerState.facing, region))
 		return;
 
-	Player::setRegionMoveTarget(_scene.state, _playerState,
-		Player::resolveRegionTargetX(_scene.state, region, _playerState),
-		Player::resolveRegionTargetZ(region));
+	_player.setRegionMoveTarget(_scene.state,
+		Player::resolveRegionTargetX(_scene.state, region, playerState), Player::resolveRegionTargetZ(region));
 	if (_engine.isPathfindingDebugEnabled()) {
 		debugC(1, kDebugPathfinding,
 			"Harvester: pathfinding region target room='%s' region='%s' target=(%d,%d,z=%.2f)",
 			_scene.state.roomName.c_str(), region.regionName.c_str(),
-			_playerState.targetX, _playerState.targetBottomY, (double)_playerState.targetZ);
+			playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ);
 	}
 }
 
 Common::Error Interaction::runRegionInteraction(const RegionRecord &region,
 		InventorySystem &inventory, bool &needsRedraw) {
+	const RoomPlayerState &playerState = _player.getState();
 	InteractionResult interaction;
 	if (!_engine.getScript()->resolveRegionInteraction(region, interaction, _scene.state.roomName))
 		return Common::kNoError;
 	debugC(2, kDebugRoom,
 		"Harvester: region interaction room='%s' region='%s' action='%s' player=(%d,%d,z=%.2f) next_room='%s'",
 		_scene.state.roomName.c_str(), region.regionName.c_str(), region.actionTag.c_str(),
-		_playerState.centerX, _playerState.bottomY, (double)_playerState.z,
+		playerState.centerX, playerState.bottomY, (double)playerState.z,
 		interaction.nextRoomName.c_str());
 
 	bool didTransition = false;
@@ -421,7 +422,8 @@ Common::Error Interaction::runRegionInteraction(const RegionRecord &region,
 }
 
 Common::Error Interaction::tryActivatePendingRegion(InventorySystem &inventory, bool &needsRedraw) {
-	if (_pendingRegionName.empty() || !_playerState.entity)
+	const RoomPlayerState &playerState = _player.getState();
+	if (_pendingRegionName.empty() || !playerState.entity)
 		return Common::kNoError;
 
 	const RegionRecord *region = findSceneRegionByName(_scene.sceneRegions, _pendingRegionName);
@@ -429,11 +431,11 @@ Common::Error Interaction::tryActivatePendingRegion(InventorySystem &inventory,
 		_pendingRegionName.clear();
 		return Common::kNoError;
 	}
-	if (!doesPlayerOverlapRegion(*_playerState.entity, *region))
+	if (!doesPlayerOverlapRegion(*playerState.entity, *region))
 		return Common::kNoError;
-	if (!doesPlayerFacingMatchRegion(_playerState.facing, *region)) {
-		if (!_playerState.hasMoveTarget && !_playerState.turnActive && region->desiredFacing >= 0)
-			(void)Player::startTurnAnimation(_playerState, region->desiredFacing);
+	if (!doesPlayerFacingMatchRegion(playerState.facing, *region)) {
+		if (!playerState.hasMoveTarget && !playerState.turnActive && region->desiredFacing >= 0)
+			(void)_player.startTurnAnimation(region->desiredFacing);
 		return Common::kNoError;
 	}
 
@@ -442,15 +444,16 @@ Common::Error Interaction::tryActivatePendingRegion(InventorySystem &inventory,
 }
 
 Common::Error Interaction::tryActivatePassiveRegion(InventorySystem &inventory, bool &needsRedraw) {
-	if (!_playerState.entity)
+	const RoomPlayerState &playerState = _player.getState();
+	if (!playerState.entity)
 		return Common::kNoError;
 
 	for (const RegionRecord &region : _scene.sceneRegions) {
 		if (!region.startEnabled)
 			continue;
-		if (!doesPlayerOverlapRegion(*_playerState.entity, region))
+		if (!doesPlayerOverlapRegion(*playerState.entity, region))
 			continue;
-		if (!doesPlayerFacingMatchRegion(_playerState.facing, region))
+		if (!doesPlayerFacingMatchRegion(playerState.facing, region))
 			continue;
 
 		_callbacks.stopPlayerRegionInteraction();
@@ -488,19 +491,18 @@ Common::Error Interaction::handleCombatInteraction(const InteractionResult &inte
 
 Common::Error Interaction::handleInteractionResult(const InteractionResult &interaction,
 		bool &didTransition, const Common::String &usedItemName, bool interruptPlayerMovement) {
+	const RoomPlayerState &playerState = _player.getState();
 	didTransition = false;
 
 	if (interruptPlayerMovement) {
-		_playerState.hasMoveTarget = false;
-		_playerState.turnActive = false;
-		_playerState.turnTargetFacing = -1;
+		_player.cancelMovement();
 		_pendingRegionName.clear();
-	} else if (_playerState.hasMoveTarget || _playerState.turnActive) {
+	} else if (playerState.hasMoveTarget || playerState.turnActive) {
 		debugC(2, kDebugPlayer,
 			"Harvester: background action preserving player movement room='%s' pos=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f) region='%s' turning=%d",
-			_scene.state.roomName.c_str(), _playerState.centerX, _playerState.bottomY,
-			_playerState.z, _playerState.targetX, _playerState.targetBottomY,
-			_playerState.targetZ, _pendingRegionName.c_str(), _playerState.turnActive);
+			_scene.state.roomName.c_str(), playerState.centerX, playerState.bottomY,
+			playerState.z, playerState.targetX, playerState.targetBottomY,
+			playerState.targetZ, _pendingRegionName.c_str(), playerState.turnActive);
 	}
 	if (interaction.requestDemoEnding) {
 		Common::Error endingError = _flow.runDemoEnding();
@@ -689,15 +691,15 @@ Common::Error Interaction::handleInteractionResult(const InteractionResult &inte
 	if (interaction.requestPlayerGotoXZ)
 		_callbacks.applyPlayerGotoXZ(interaction.playerGotoX, interaction.playerGotoZ);
 	else if (interaction.requestPlayerGotoZ)
-		_callbacks.applyPlayerGotoXZ(_playerState.centerX, interaction.playerGotoZ);
+		_callbacks.applyPlayerGotoXZ(playerState.centerX, interaction.playerGotoZ);
 
 	if (!interaction.moveEntityToPlayerZName.empty()) {
 		EntityManager *entityManager = _engine.getRuntimeEntities();
 		Entity *entity = entityManager
 			? entityManager->findSceneEntityByName(interaction.moveEntityToPlayerZName)
 			: nullptr;
-		if (entity && _playerState.entity) {
-			entity->setPosition(entity->getX(), entity->getY(), _playerState.entity->getZ());
+		if (entity && playerState.entity) {
+			entity->setPosition(entity->getX(), entity->getY(), playerState.entity->getZ());
 			entityManager->reinsertSceneEntity(entity);
 			debugC(1, kDebugPlayer, "Harvester: MOVE_BM2PCZ entity='%s' z=%.2f",
 				interaction.moveEntityToPlayerZName.c_str(), entity->getZ());
diff --git a/engines/harvester/interaction.h b/engines/harvester/interaction.h
index 419031cf7d8..23150d27f58 100644
--- a/engines/harvester/interaction.h
+++ b/engines/harvester/interaction.h
@@ -34,6 +34,7 @@ class HarvesterEngine;
 class InventorySystem;
 class Interaction;
 class SceneEntities;
+class Player;
 struct IndexedBitmap;
 
 extern const char *const kExitCloseupPendingRoomChange;
@@ -102,7 +103,7 @@ private:
 class Interaction {
 public:
 	Interaction(HarvesterEngine &engine, Flow &flow, SceneState &scene,
-		RoomPlayerState &playerState, Common::String &pendingRegionName,
+		Player &player, Common::String &pendingRegionName,
 		Common::String &pendingRoomChange, bool &pendingRoomChangeIsRoomName,
 		bool &pendingRoomChangeUsesSavedRoomState, SaveRoomState &pendingRoomChangeSavedRoomState,
 		bool canExitCloseupToParent, InteractionCallbacks &callbacks);
@@ -125,7 +126,7 @@ private:
 	HarvesterEngine &_engine;
 	Flow &_flow;
 	SceneState &_scene;
-	RoomPlayerState &_playerState;
+	Player &_player;
 	Common::String &_pendingRegionName;
 	Common::String &_pendingRoomChange;
 	bool &_pendingRoomChangeIsRoomName;
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 0136cc01933..c039844bd9c 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -22,6 +22,7 @@
 #include <math.h>
 
 #include "harvester/player.h"
+#include "harvester/combat.h"
 
 #include "common/debug.h"
 #include "common/system.h"
@@ -34,6 +35,10 @@ namespace Harvester {
 
 namespace {
 
+static void syncPlayerAnimationPlacement(RoomPlayerState &playerState);
+static bool setPlayerIdleAnimation(RoomPlayerState &playerState, int facing);
+static void updatePlayerIdleTrigger(RoomIdleAnimationState &idleState);
+
 static const char *const kPlayerActorResourcePath = "1:/GRAPHIC/MONSTERS/PC/PC0.ABM";
 static const float kRoomPlayerHorizontalMoveBase = 8.0f;
 static const float kRoomPlayerHorizontalTargetSlackBase = 50.0f;
@@ -295,7 +300,7 @@ static bool playPlayerHitSound(HarvesterEngine &engine) {
 	return engine.playSound(kPlayerHitSoundPaths[soundIndex]);
 }
 
-static bool runtimeEntityHasFrameRange(const Entity &entity, int firstFrame, int lastFrame) {
+static bool playerEntityHasFrameRange(const Entity &entity, int firstFrame, int lastFrame) {
 	if (!entity.hasFrames() || firstFrame < 0 || lastFrame < firstFrame)
 		return false;
 
@@ -304,7 +309,7 @@ static bool runtimeEntityHasFrameRange(const Entity &entity, int firstFrame, int
 
 static bool choosePlayerDeathAnimationRange(const Entity &entity, int firstFrame, int lastFrame,
 		PlayerDeathAnimationRange &range) {
-	if (!runtimeEntityHasFrameRange(entity, firstFrame, lastFrame))
+	if (!playerEntityHasFrameRange(entity, firstFrame, lastFrame))
 		return false;
 
 	range = PlayerDeathAnimationRange(firstFrame, lastFrame);
@@ -481,7 +486,7 @@ static bool resolvePlayerHitAnimationRange(const Entity &entity,
 		return false;
 	}
 
-	if (!runtimeEntityHasFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
+	if (!playerEntityHasFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
 		return false;
 
 	range = candidate;
@@ -830,7 +835,7 @@ static bool tryApplyPlayerMovement(HarvesterEngine &engine, const RoomSetupState
 
 	applyPlayerMovementPlacement(state, playerState, candidateCenterX, candidateBottomY, candidateZ);
 	playerState.z = candidateZ;
-	Player::syncAnimationPlacement(playerState);
+	syncPlayerAnimationPlacement(playerState);
 
 	reinsertPlayerSceneEntity(engine, playerState);
 	return true;
@@ -1011,7 +1016,7 @@ static bool tryApplyPlayerWalkMovement(HarvesterEngine &engine, const RoomSetupS
 	// Region activation gets the resulting frame before the next blocker response.
 	applyPlayerMovementPlacement(state, playerState, candidateCenterX, candidateBottomY, candidateZ);
 	playerState.z = candidateZ;
-	Player::syncAnimationPlacement(playerState);
+	syncPlayerAnimationPlacement(playerState);
 	reinsertPlayerSceneEntity(engine, playerState);
 	return true;
 }
@@ -1040,7 +1045,7 @@ static void setMoveTargetInternal(const RoomSetupState &state, RoomPlayerState &
 
 static void notePlayerIdleReset(RoomIdleAnimationState &idleState) {
 	idleState.resetTick = Player::getRuntimeClockTicks();
-	Player::updateIdleTrigger(idleState);
+	updatePlayerIdleTrigger(idleState);
 }
 
 static bool setPlayerWalkAnimation(RoomPlayerState &playerState, int facing) {
@@ -1115,7 +1120,7 @@ static bool finishPlayerIdleAnimation(const RoomSetupState &state, RoomPlayerSta
 	if (playerState.entity) {
 		playerState.entity->setVisible(true);
 		if (idleState.restoreFacing >= 0)
-			(void)Player::setIdleAnimation(playerState, idleState.restoreFacing);
+			(void)setPlayerIdleAnimation(playerState, idleState.restoreFacing);
 	}
 
 	idleState.active = false;
@@ -1129,10 +1134,180 @@ static bool finishPlayerIdleAnimation(const RoomSetupState &state, RoomPlayerSta
 	return true;
 }
 
+static void syncPlayerAnimationPlacement(RoomPlayerState &playerState) {
+	if (!playerState.entity)
+		return;
+
+	const Common::Rect rect = playerState.entity->getScreenRect();
+	const int centerX = rect.left + rect.width() / 2;
+	if (playerState.centerX != centerX || playerState.bottomY != rect.bottom) {
+		debugC(3, kDebugPlayer,
+			"Harvester: player frame bounds frame=%d base=(%d,%d) rect=(%d,%d)-(%d,%d) z=%.2f",
+			playerState.entity->getCurrentFrame(), playerState.entity->getX(), playerState.entity->getY(),
+			rect.left, rect.top, rect.right, rect.bottom, (double)playerState.z);
+	}
+	playerState.centerX = centerX;
+	playerState.bottomY = rect.bottom;
+}
+
+static void updatePlayerIdleTrigger(RoomIdleAnimationState &idleState) {
+	idleState.triggerTick = MAX(idleState.activityTick, idleState.resetTick) + kRoomPlayerIdleDelayTicks;
+}
+
+static bool setPlayerIdleAnimation(RoomPlayerState &playerState, int facing) {
+	if (!playerState.entity)
+		return false;
+
+	const PlayerAnimationRange range = resolvePlayerAnimationRange(facing);
+	const bool changed = playerState.facing != facing ||
+		playerState.entity->getCurrentFrame() != range.idleFrame ||
+		playerState.entity->getAnimationRate() != 0;
+	playerState.facing = facing;
+	playerState.nextMovementTick = 0;
+	playerState.keyboardWalkRequest = -1;
+	playerState.walkHorizontalDirection = 0;
+	playerState.walkDepthDirection = 0;
+	playerState.entity->setAnimationRate(0);
+	playerState.entity->setAnimationFrameRange(range.idleFrame, range.idleFrame, false);
+	playerState.entity->setCurrentFrame(range.idleFrame);
+	if (changed) {
+		debugC(1, kDebugPlayer,
+			"Harvester: player idle animation facing=%d frame=%d",
+			facing, range.idleFrame);
+	}
+	return changed;
+}
+
 } // End of anonymous namespace
 
-void Player::rebindAfterSceneRefresh(const RoomSetupState &state, RoomPlayerState &playerState,
+void Player::initialize(const RoomSetupState &state, Entity *entity, int combatLoadout) {
+	RoomPlayerState &playerState = _state;
+	playerState.entity = entity;
+	playerState.centerX = state.playerSpawnX;
+	playerState.bottomY = state.playerSpawnY;
+	playerState.z = (float)state.playerSpawnZ;
+	playerState.facing = state.playerFacing;
+	playerState.combatLoadout = combatLoadout;
+	playerState.turnActive = false;
+	playerState.turnTargetFacing = -1;
+}
+
+bool Player::isCombatLocked() const {
+	return _state.attackActive || _state.hitActive || _state.deathActive;
+}
+
+void Player::cancelMovement() {
+	_state.hasMoveTarget = false;
+	_state.turnActive = false;
+	_state.turnTargetFacing = -1;
+}
+
+void Player::cancelMovementForAttack() {
+	cancelMovement();
+	_state.turnFirstFrame = -1;
+	_state.turnLastFrame = -1;
+	_state.turnEndFrame = -1;
+	_state.turnPlayBackwards = false;
+}
+
+bool Player::cancelAttackAnimation() {
+	RoomPlayerState &playerState = _state;
+	if (!playerState.attackActive)
+		return false;
+
+	playerState.attackActive = false;
+	playerState.attackFirstFrame = -1;
+	playerState.attackLastFrame = -1;
+	playerState.attackContactFrame = -1;
+	if (!playerState.entity)
+		return false;
+	playerState.attackSoundPlayed = false;
+	playerState.attackSoundPlaybackFrame = -1;
+	playerState.attackContactResolved = false;
+	playerState.attackTargetName.clear();
+	playerState.attackTargetClassId = -1;
+	const int resumeFacing = playerState.attackResumeFacing >= 0
+		? playerState.attackResumeFacing
+		: (playerState.facing >= 0 ? playerState.facing : 0);
+	playerState.attackResumeFacing = -1;
+	return setIdleAnimation(resumeFacing);
+}
+
+void Player::clearAttackTarget() {
+	_state.attackTargetName.clear();
+	_state.attackTargetClassId = -1;
+}
+
+void Player::captureAttackTarget(const Common::String &name, int classId) {
+	_state.attackTargetName = name;
+	_state.attackTargetClassId = classId;
+}
+
+void Player::markAttackContactResolved() {
+	_state.attackContactResolved = true;
+}
+
+void Player::resetIdleState() {
+	RoomIdleAnimationState &idleState = _idleState;
+	idleState = RoomIdleAnimationState();
+	idleState.activityTick = Player::getRuntimeClockTicks();
+	idleState.resetTick = idleState.activityTick;
+	updatePlayerIdleTrigger(idleState);
+}
+
+void Player::noteActivity() {
+	RoomIdleAnimationState &idleState = _idleState;
+	idleState.activityTick = Player::getRuntimeClockTicks();
+	updatePlayerIdleTrigger(idleState);
+}
+
+void Player::cancelIdleAnimation() {
+	RoomPlayerState &playerState = _state;
+	RoomIdleAnimationState &idleState = _idleState;
+	if (idleState.entity) {
+		idleState.entity->setVisible(false);
+		idleState.entity->setAnimationRate(0);
+	}
+	idleState.active = false;
+	idleState.loopStarted = false;
+	idleState.exiting = false;
+	idleState.restoreFacing = -1;
+	if (playerState.entity)
+		playerState.entity->setVisible(true);
+}
+
+void Player::applyGotoXZ(const RoomSetupState &state, EntityManager *entityManager, int x, int z) {
+	RoomPlayerState &playerState = _state;
+	playerState.hasMoveTarget = false;
+	playerState.turnActive = false;
+	playerState.turnTargetFacing = -1;
+	playerState.centerX = x;
+	playerState.z = (float)z;
+	playerState.bottomY = mapRoomDepthToScreenYForCombat(
+		state, playerState.z, playerState.bottomY);
+	Common::Rect screenRect;
+	bool hasScreenRect = false;
+	float depthScale = 0.0f;
+	if (playerState.entity) {
+		(void)applyRoomActorPlacement(state, *playerState.entity,
+			playerState.centerX, playerState.bottomY, playerState.z);
+		if (entityManager)
+			entityManager->reinsertSceneEntity(playerState.entity);
+		screenRect = playerState.entity->getScreenRect();
+		hasScreenRect = true;
+		depthScale = playerState.entity->getDepthScale();
+	}
+	debugC(1, kDebugPlayer,
+		"Harvester: PC_GOTO_XZ applied x=%d z=%d pos=(%d,%d,z=%.2f) screen_rect=(%d,%d)-(%d,%d) depth_scale=%.3f",
+		x, z, playerState.centerX, playerState.bottomY, (double)playerState.z,
+		hasScreenRect ? screenRect.left : 0, hasScreenRect ? screenRect.top : 0,
+		hasScreenRect ? screenRect.right : 0, hasScreenRect ? screenRect.bottom : 0,
+		(double)depthScale);
+}
+
+void Player::rebindAfterSceneRefresh(HarvesterEngine &engine, const RoomSetupState &state,
 		EntityManager *entityManager, bool preservePlayerPlacement) {
+	RoomPlayerState &playerState = _state;
 	playerState.entity = entityManager ? entityManager->findSceneEntityByName("PLAYER") : nullptr;
 	if (playerState.entity) {
 		if (!preservePlayerPlacement || playerState.facing < 0) {
@@ -1143,7 +1318,7 @@ void Player::rebindAfterSceneRefresh(const RoomSetupState &state, RoomPlayerStat
 		}
 
 		const int facing = playerState.facing >= 0 ? playerState.facing : state.playerFacing;
-		(void)setIdleAnimation(playerState, facing);
+		(void)setIdleAnimation(facing);
 		(void)applyRoomActorPlacement(state, *playerState.entity,
 			playerState.centerX, playerState.bottomY, playerState.z);
 	}
@@ -1170,22 +1345,11 @@ void Player::rebindAfterSceneRefresh(const RoomSetupState &state, RoomPlayerStat
 	playerState.hitKnockbackDecayStep = 0;
 	debugC(2, kDebugPlayer, "Harvester: rebound player after scene refresh room='%s' entity=%d preserve_placement=%d",
 		state.roomName.c_str(), playerState.entity != nullptr, preservePlayerPlacement);
+	playerState.combatLoadout = engine.getScript() ? engine.getScript()->getPlayerCombatLoadout() : 0;
 }
 
-void Player::syncAnimationPlacement(RoomPlayerState &playerState) {
-	if (!playerState.entity)
-		return;
-
-	const Common::Rect rect = playerState.entity->getScreenRect();
-	const int centerX = rect.left + rect.width() / 2;
-	if (playerState.centerX != centerX || playerState.bottomY != rect.bottom) {
-		debugC(3, kDebugPlayer,
-			"Harvester: player frame bounds frame=%d base=(%d,%d) rect=(%d,%d)-(%d,%d) z=%.2f",
-			playerState.entity->getCurrentFrame(), playerState.entity->getX(), playerState.entity->getY(),
-			rect.left, rect.top, rect.right, rect.bottom, (double)playerState.z);
-	}
-	playerState.centerX = centerX;
-	playerState.bottomY = rect.bottom;
+void Player::syncAnimationPlacement() {
+	syncPlayerAnimationPlacement(_state);
 }
 
 float Player::computeDepthScale(const RoomSetupState &state, float z) {
@@ -1283,33 +1447,29 @@ bool Player::isIdleAnimationExcludedRoom(const Common::String &roomName) {
 	return false;
 }
 
-void Player::updateIdleTrigger(RoomIdleAnimationState &idleState) {
-	idleState.triggerTick = MAX(idleState.activityTick, idleState.resetTick) + kRoomPlayerIdleDelayTicks;
-}
 
-void Player::setMoveTarget(const RoomSetupState &state, RoomPlayerState &playerState,
-		int targetX, float targetZ) {
+void Player::setMoveTarget(const RoomSetupState &state, int targetX, float targetZ) {
+	RoomPlayerState &playerState = _state;
 	setMoveTargetInternal(state, playerState, targetX, targetZ,
 		mapRoomDepthToScreenY(state, targetZ));
 }
 
-void Player::setRegionMoveTarget(const RoomSetupState &state, RoomPlayerState &playerState,
-		int targetX, float targetZ) {
+void Player::setRegionMoveTarget(const RoomSetupState &state, int targetX, float targetZ) {
+	RoomPlayerState &playerState = _state;
 	setMoveTargetInternal(state, playerState, targetX, targetZ,
 		mapRoomDepthToScreenY(state, targetZ));
 }
 
-void Player::setMoveTargetFromScreenPoint(const RoomSetupState &state,
-		RoomPlayerState &playerState, int targetX, int targetBottomY) {
+void Player::setMoveTargetFromScreenPoint(const RoomSetupState &state, int targetX, int targetBottomY) {
+	RoomPlayerState &playerState = _state;
 	const int clampedTargetBottomY = clampRoomMovementY(state, targetBottomY);
 	setMoveTargetInternal(state, playerState, targetX,
 		mapRoomScreenYToDepth(state, clampedTargetBottomY), clampedTargetBottomY);
 }
 
 bool Player::resolveBlockedStartupSpawn(HarvesterEngine &engine, const RoomSetupState &state,
-		const Common::Array<ObjectRecord> &sceneObjects,
-		const Common::Array<AnimRecord> &sceneAnimations,
-		RoomPlayerState &playerState) {
+		const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations) {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.entity)
 		return false;
 
@@ -1381,8 +1541,16 @@ float Player::resolveRegionTargetZ(const RegionRecord &region) {
 	return (float)(region.minZ + (region.maxZ - region.minZ) / 2);
 }
 
-bool Player::syncCombatLoadoutVisual(HarvesterEngine &engine, const RoomSetupState &state,
-		RoomPlayerState &playerState, int loadout) {
+void Player::syncCombatLoadout(HarvesterEngine &engine, const RoomSetupState &state) {
+	if (_state.entity) {
+		(void)syncCombatLoadoutVisual(engine, state, engine.getScript()->getPlayerCombatLoadout());
+	} else if (engine.getScript()) {
+		_state.combatLoadout = engine.getScript()->getPlayerCombatLoadout();
+	}
+}
+
+bool Player::syncCombatLoadoutVisual(HarvesterEngine &engine, const RoomSetupState &state, int loadout) {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.entity)
 		return false;
 
@@ -1415,7 +1583,7 @@ bool Player::syncCombatLoadoutVisual(HarvesterEngine &engine, const RoomSetupSta
 	playerState.hitResumeFacing = -1;
 	playerState.hitKnockbackRemainingX = 0;
 	playerState.hitKnockbackDecayStep = 0;
-	(void)setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
+	(void)setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
 	if (applyRoomActorPlacement(state, *playerState.entity,
 			playerState.centerX, playerState.bottomY, playerState.z)) {
 		reinsertPlayerSceneEntity(engine, playerState);
@@ -1430,32 +1598,12 @@ bool Player::syncCombatLoadoutVisual(HarvesterEngine &engine, const RoomSetupSta
 	return true;
 }
 
-bool Player::setIdleAnimation(RoomPlayerState &playerState, int facing) {
-	if (!playerState.entity)
-		return false;
-
-	const PlayerAnimationRange range = resolvePlayerAnimationRange(facing);
-	const bool changed = playerState.facing != facing ||
-		playerState.entity->getCurrentFrame() != range.idleFrame ||
-		playerState.entity->getAnimationRate() != 0;
-	playerState.facing = facing;
-	playerState.nextMovementTick = 0;
-	playerState.keyboardWalkRequest = -1;
-	playerState.walkHorizontalDirection = 0;
-	playerState.walkDepthDirection = 0;
-	playerState.entity->setAnimationRate(0);
-	playerState.entity->setAnimationFrameRange(range.idleFrame, range.idleFrame, false);
-	playerState.entity->setCurrentFrame(range.idleFrame);
-	if (changed) {
-		debugC(1, kDebugPlayer,
-			"Harvester: player idle animation facing=%d frame=%d",
-			facing, range.idleFrame);
-	}
-	return changed;
+bool Player::setIdleAnimation(int facing) {
+	return setPlayerIdleAnimation(_state, facing);
 }
 
-bool Player::startAttackAnimation(const RoomSetupState &state,
-		RoomPlayerState &playerState, const Common::Point &mousePos) {
+bool Player::startAttackAnimation(const RoomSetupState &state, const Common::Point &mousePos) {
+	RoomPlayerState &playerState = _state;
 	PlayerAttackAnimationRange range;
 	if (!resolvePlayerAttackAnimationRange(state, playerState, mousePos, range))
 		return false;
@@ -1468,7 +1616,8 @@ bool Player::startAttackAnimation(const RoomSetupState &state,
 }
 
 bool Player::startKeyboardAttackAnimation(const RoomSetupState &state,
-		RoomPlayerState &playerState, bool attackLeft, bool attackRight, bool attackUp, bool attackDown) {
+		bool attackLeft, bool attackRight, bool attackUp, bool attackDown) {
+	RoomPlayerState &playerState = _state;
 	(void)state;
 
 	PlayerAttackAnimationRange range;
@@ -1483,8 +1632,8 @@ bool Player::startKeyboardAttackAnimation(const RoomSetupState &state,
 	return true;
 }
 
-bool Player::updateAttackAnimationState(HarvesterEngine &engine,
-		RoomPlayerState &playerState) {
+bool Player::updateAttackAnimationState(HarvesterEngine &engine) {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.attackActive || !playerState.entity)
 		return false;
 	bool changed = false;
@@ -1527,11 +1676,11 @@ bool Player::updateAttackAnimationState(HarvesterEngine &engine,
 	playerState.attackContactResolved = false;
 	playerState.attackTargetName.clear();
 	playerState.attackTargetClassId = -1;
-	return setIdleAnimation(playerState, resumeFacing) || changed;
+	return setIdleAnimation(resumeFacing) || changed;
 }
 
-bool Player::startHitAnimation(HarvesterEngine &engine, RoomPlayerState &playerState,
-		int monsterAttackFirstFrame) {
+bool Player::startHitAnimation(HarvesterEngine &engine, int monsterAttackFirstFrame) {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.entity || playerState.deathActive)
 		return false;
 
@@ -1577,9 +1726,8 @@ bool Player::startHitAnimation(HarvesterEngine &engine, RoomPlayerState &playerS
 }
 
 bool Player::updateHitAnimationState(HarvesterEngine &engine, const RoomSetupState &state,
-		const Common::Array<ObjectRecord> &sceneObjects,
-		const Common::Array<AnimRecord> &sceneAnimations,
-		RoomPlayerState &playerState) {
+		const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations) {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.hitActive || !playerState.entity)
 		return false;
 
@@ -1612,10 +1760,11 @@ bool Player::updateHitAnimationState(HarvesterEngine &engine, const RoomSetupSta
 	playerState.hitResumeFacing = -1;
 	playerState.hitKnockbackRemainingX = 0;
 	playerState.hitKnockbackDecayStep = 0;
-	return setIdleAnimation(playerState, resumeFacing) || changed;
+	return setIdleAnimation(resumeFacing) || changed;
 }
 
-bool Player::startDeathAnimation(RoomPlayerState &playerState, int damageType, bool goreEnabled) {
+bool Player::startDeathAnimation(int damageType, bool goreEnabled) {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.entity || playerState.deathActive)
 		return false;
 
@@ -1666,7 +1815,8 @@ bool Player::startDeathAnimation(RoomPlayerState &playerState, int damageType, b
 	return true;
 }
 
-bool Player::updateDeathAnimationState(RoomPlayerState &playerState) {
+bool Player::updateDeathAnimationState() {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.deathActive || !playerState.entity)
 		return false;
 
@@ -1691,7 +1841,8 @@ bool Player::updateDeathAnimationState(RoomPlayerState &playerState) {
 	return true;
 }
 
-bool Player::startTurnAnimation(RoomPlayerState &playerState, int targetFacing) {
+bool Player::startTurnAnimation(int targetFacing) {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.entity || playerState.hitActive ||
 			playerState.facing < 0 || playerState.facing == targetFacing)
 		return false;
@@ -1728,7 +1879,8 @@ bool Player::startTurnAnimation(RoomPlayerState &playerState, int targetFacing)
 	return true;
 }
 
-bool Player::updateTurnAnimationState(RoomPlayerState &playerState) {
+bool Player::updateTurnAnimationState() {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.turnActive || !playerState.entity)
 		return false;
 	if (playerState.entity->getCurrentFrame() != playerState.turnEndFrame)
@@ -1749,9 +1901,9 @@ bool Player::updateTurnAnimationState(RoomPlayerState &playerState) {
 }
 
 bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state,
-		const Common::Array<ObjectRecord> &sceneObjects,
-		const Common::Array<AnimRecord> &sceneAnimations,
-		RoomPlayerState &playerState, int regionFacing) {
+		const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations,
+		int regionFacing) {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.entity || !playerState.hasMoveTarget || playerState.turnActive || playerState.hitActive)
 		return false;
 
@@ -1771,7 +1923,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
 		updatePlayerMoveTargetProgress(state, playerState, regionFacing);
 	if (hasPlayerReachedMoveTarget(playerState)) {
 		playerState.hasMoveTarget = false;
-		return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
+		return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
 	}
 
 	const int previousCenterX = playerState.centerX;
@@ -1808,12 +1960,12 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
 	}
 	if (horizontalDirection == 0 && depthDirection == 0) {
 		playerState.hasMoveTarget = false;
-		return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
+		return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
 	}
 	const int desiredFacing = resolveFacingFromMoveDirection(
 		horizontalDirection, depthDirection, playerState.facing);
 	if (regionFacing >= 0 && desiredFacing != playerState.facing &&
-			startTurnAnimation(playerState, desiredFacing)) {
+			startTurnAnimation(desiredFacing)) {
 		debugC(2, kDebugPlayer,
 			"Harvester: player region target turn room='%s' required_facing=%d movement_facing=%d target=(%d,z=%.2f)",
 			state.roomName.c_str(), regionFacing, desiredFacing,
@@ -1871,9 +2023,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
 		}
 		setPlayerCurrentBlocker(playerState, blocker);
 		if (blocker) {
-			return setIdleAnimation(playerState,
-				resolveFacingFromMoveDirection(horizontalDirection, depthDirection,
-					playerState.facing));
+			return setIdleAnimation(resolveFacingFromMoveDirection(horizontalDirection, depthDirection, playerState.facing));
 		}
 
 		playerState.hasMoveTarget = false;
@@ -1884,7 +2034,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
 			else
 				idleFacing = depthDirection < 0 ? 3 : 0;
 		}
-		return setIdleAnimation(playerState, idleFacing);
+		return setIdleAnimation(idleFacing);
 	}
 
 	const int actualFacing = desiredFacing;
@@ -1898,7 +2048,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
 			actualFacing, playerState.entity->getCurrentFrame(), (double)computePlayerRenderZ(playerState),
 			horizontalDirection, depthDirection);
 	}
-	syncAnimationPlacement(playerState);
+	syncAnimationPlacement();
 	refreshPlayerCurrentBlocker(engine, sceneObjects, sceneAnimations, playerState);
 	if (engine.isPathfindingDebugEnabled()) {
 		debugC(1, kDebugPathfinding,
@@ -1917,9 +2067,9 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
 }
 
 bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState &state,
-		const Common::Array<ObjectRecord> &sceneObjects,
-		const Common::Array<AnimRecord> &sceneAnimations,
-		RoomPlayerState &playerState, bool moveLeft, bool moveRight, bool moveUp, bool moveDown) {
+		const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations,
+		bool moveLeft, bool moveRight, bool moveUp, bool moveDown) {
+	RoomPlayerState &playerState = _state;
 	if (!playerState.entity || !supportsMovementBand(state))
 		return false;
 	if (playerState.turnActive || playerState.hitActive)
@@ -1948,7 +2098,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
 	if (horizontalInput == 0 && verticalInput == 0) {
 		if (playerState.hasMoveTarget || playerState.facing < 0)
 			return false;
-		return setIdleAnimation(playerState, playerState.facing);
+		return setIdleAnimation(playerState.facing);
 	}
 
 	const int previousBaseX = playerState.entity->getX();
@@ -1976,8 +2126,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
 			fabsf(candidateZ - playerState.z) <= kRoomDepthCompareEpsilon) {
 		playerState.hasMoveTarget = false;
 		if (depthBoundaryReached) {
-			const bool stopped = setIdleAnimation(
-				playerState, playerState.facing >= 0 ? playerState.facing : 0);
+			const bool stopped = setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
 			if (stopped) {
 				debugC(3, kDebugPathfinding,
 					"Harvester: player keyboard depth boundary room='%s' input_y=%d pos=(%d,%d,z=%.2f) bounds=(%d,%d)",
@@ -1987,12 +2136,12 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
 			}
 			return stopped;
 		}
-		return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
+		return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
 	}
 
 	const int desiredFacing = resolveFacingFromRoomMovement(
 		playerState.centerX, playerState.bottomY, candidateCenterX, candidateBottomY);
-	if (desiredFacing != playerState.facing && startTurnAnimation(playerState, desiredFacing)) {
+	if (desiredFacing != playerState.facing && startTurnAnimation(desiredFacing)) {
 		playerState.keyboardWalkRequest = -1;
 		return true;
 	}
@@ -2005,7 +2154,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
 		playerState, candidateCenterX, candidateBottomY, candidateZ,
 		horizontalInput, verticalInput);
 	if (!moved)
-		return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
+		return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
 
 	const int actualFacing = resolveFacingFromRoomMovement(
 		previousBaseX, previousBaseY, playerState.entity->getX(), playerState.entity->getY());
@@ -2019,7 +2168,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
 			"Harvester: player keyboard walk bank restart facing=%d frame=%d",
 			actualFacing, playerState.entity->getCurrentFrame());
 	}
-	syncAnimationPlacement(playerState);
+	syncAnimationPlacement();
 	debugC(2, kDebugPlayer,
 		"Harvester: player keyboard move room='%s' input=(%d,%d) step=(%d,%d) pos=(%d,%d,z=%.2f) facing=%d frame=%d moved=%d",
 		state.roomName.c_str(), (moveRight ? 1 : 0) - (moveLeft ? 1 : 0),
@@ -2029,8 +2178,9 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
 	return true;
 }
 
-bool Player::requestIdleAnimationExit(const RoomSetupState &state,
-		RoomPlayerState &playerState, RoomIdleAnimationState &idleState) {
+bool Player::requestIdleAnimationExit(const RoomSetupState &state) {
+	RoomPlayerState &playerState = _state;
+	RoomIdleAnimationState &idleState = _idleState;
 	if (!idleState.active || idleState.exiting || !idleState.entity)
 		return false;
 
@@ -2051,8 +2201,9 @@ bool Player::requestIdleAnimationExit(const RoomSetupState &state,
 	return true;
 }
 
-bool Player::startIdleAnimation(HarvesterEngine &engine, const RoomSetupState &state,
-		RoomPlayerState &playerState, RoomIdleAnimationState &idleState) {
+bool Player::startIdleAnimation(HarvesterEngine &engine, const RoomSetupState &state) {
+	RoomPlayerState &playerState = _state;
+	RoomIdleAnimationState &idleState = _idleState;
 	if (!playerState.entity || idleState.active || isIdleAnimationExcludedRoom(state.roomName))
 		return false;
 
@@ -2082,8 +2233,9 @@ bool Player::startIdleAnimation(HarvesterEngine &engine, const RoomSetupState &s
 	return true;
 }
 
-bool Player::updateIdleAnimation(const RoomSetupState &state,
-		RoomPlayerState &playerState, RoomIdleAnimationState &idleState) {
+bool Player::updateIdleAnimation(const RoomSetupState &state) {
+	RoomPlayerState &playerState = _state;
+	RoomIdleAnimationState &idleState = _idleState;
 	if (!idleState.active || !idleState.entity)
 		return false;
 
diff --git a/engines/harvester/player.h b/engines/harvester/player.h
index 414ac7337fd..25cacd7a423 100644
--- a/engines/harvester/player.h
+++ b/engines/harvester/player.h
@@ -28,11 +28,92 @@ namespace Harvester {
 
 class EntityManager;
 
+struct RoomPlayerState {
+	Entity *entity = nullptr;
+	int centerX = 0;
+	int bottomY = 0;
+	float z = 0.0f;
+	int facing = -1;
+	int combatLoadout = 0;
+	uint32 nextMovementTick = 0;
+	int keyboardWalkRequest = -1;
+	int walkHorizontalDirection = 0;
+	int walkDepthDirection = 0;
+	bool hasMoveTarget = false;
+	int targetX = 0;
+	int targetBottomY = 0;
+	float targetZ = 0.0f;
+	bool moveTargetXReached = false;
+	bool moveTargetZReached = false;
+	Common::String currentBlockerName;
+
+	Common::String upwardBlockerHistory;
+	Common::String downwardBlockerHistory;
+	Common::String leftwardBlockerHistory;
+	Common::String rightwardBlockerHistory;
+	bool turnActive = false;
+	int turnTargetFacing = -1;
+	int turnFirstFrame = -1;
+	int turnLastFrame = -1;
+	int turnEndFrame = -1;
+	bool turnPlayBackwards = false;
+	bool attackActive = false;
+	int attackFirstFrame = -1;
+	int attackLastFrame = -1;
+	int attackContactFrame = -1;
+	int attackResumeFacing = -1;
+	bool attackSoundPlayed = false;
+	int attackSoundPlaybackFrame = -1;
+	bool attackContactResolved = false;
+
+	Common::String attackTargetName;
+	int attackTargetClassId = -1;
+	bool hitActive = false;
+	int hitFirstFrame = -1;
+	int hitLastFrame = -1;
+	int hitResumeFacing = -1;
+	int hitKnockbackRemainingX = 0;
+	int hitKnockbackDecayStep = 0;
+	bool deathActive = false;
+	int deathFirstFrame = -1;
+	int deathLastFrame = -1;
+	int deathDamageType = 0;
+};
+
+struct RoomIdleAnimationState {
+	Entity *entity = nullptr;
+	int restoreFacing = -1;
+	uint32 activityTick = 0;
+	uint32 resetTick = 0;
+	uint32 triggerTick = 0;
+	bool active = false;
+	bool loopStarted = false;
+	bool exiting = false;
+};
+
 class Player {
 public:
-	static void rebindAfterSceneRefresh(const RoomSetupState &state, RoomPlayerState &playerState,
+	Player() = default;
+	Player(const Player &) = delete;
+	Player &operator=(const Player &) = delete;
+
+	const RoomPlayerState &getState() const { return _state; }
+	const RoomIdleAnimationState &getIdleState() const { return _idleState; }
+	void initialize(const RoomSetupState &state, Entity *entity, int combatLoadout);
+	bool isCombatLocked() const;
+	void cancelMovement();
+	void cancelMovementForAttack();
+	bool cancelAttackAnimation();
+	void clearAttackTarget();
+	void captureAttackTarget(const Common::String &name, int classId);
+	void markAttackContactResolved();
+	void resetIdleState();
+	void noteActivity();
+	void cancelIdleAnimation();
+	void applyGotoXZ(const RoomSetupState &state, EntityManager *entityManager, int x, int z);
+	void rebindAfterSceneRefresh(HarvesterEngine &engine, const RoomSetupState &state,
 		EntityManager *entityManager, bool preservePlayerPlacement);
-	static void syncAnimationPlacement(RoomPlayerState &playerState);
+	void syncAnimationPlacement();
 	static float computeDepthScale(const RoomSetupState &state, float z);
 	static int resolveFacingFrame(int facing);
 	static const char *describeCombatLoadout(int loadout);
@@ -45,52 +126,42 @@ public:
 	static bool supportsMovementBand(const RoomSetupState &state);
 	static uint32 getRuntimeClockTicks();
 	static bool isIdleAnimationExcludedRoom(const Common::String &roomName);
-	static void updateIdleTrigger(RoomIdleAnimationState &idleState);
-	static void setMoveTarget(const RoomSetupState &state, RoomPlayerState &playerState,
-		int targetX, float targetZ);
-	static void setRegionMoveTarget(const RoomSetupState &state, RoomPlayerState &playerState,
-		int targetX, float targetZ);
-	static void setMoveTargetFromScreenPoint(const RoomSetupState &state,
-		RoomPlayerState &playerState, int targetX, int targetBottomY);
-	static bool resolveBlockedStartupSpawn(HarvesterEngine &engine, const RoomSetupState &state,
-		const Common::Array<ObjectRecord> &sceneObjects,
-		const Common::Array<AnimRecord> &sceneAnimations,
-		RoomPlayerState &playerState);
+
+	void setMoveTarget(const RoomSetupState &state, int targetX, float targetZ);
+	void setRegionMoveTarget(const RoomSetupState &state, int targetX, float targetZ);
+	void setMoveTargetFromScreenPoint(const RoomSetupState &state, int targetX, int targetBottomY);
+	bool resolveBlockedStartupSpawn(HarvesterEngine &engine, const RoomSetupState &state,
+		const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations);
 	static int resolveRegionTargetX(const RoomSetupState &state, const RegionRecord &region,
 		const RoomPlayerState &playerState);
 	static float resolveRegionTargetZ(const RegionRecord &region);
-	static bool syncCombatLoadoutVisual(HarvesterEngine &engine, const RoomSetupState &state,
-		RoomPlayerState &playerState, int loadout);
-	static bool setIdleAnimation(RoomPlayerState &playerState, int facing);
-	static bool startAttackAnimation(const RoomSetupState &state,
-		RoomPlayerState &playerState, const Common::Point &mousePos);
-	static bool startKeyboardAttackAnimation(const RoomSetupState &state,
-		RoomPlayerState &playerState, bool attackLeft, bool attackRight, bool attackUp, bool attackDown);
-	static bool updateAttackAnimationState(HarvesterEngine &engine, RoomPlayerState &playerState);
-	static bool startHitAnimation(HarvesterEngine &engine, RoomPlayerState &playerState,
-		int monsterAttackFirstFrame);
-	static bool updateHitAnimationState(HarvesterEngine &engine, const RoomSetupState &state,
-		const Common::Array<ObjectRecord> &sceneObjects,
-		const Common::Array<AnimRecord> &sceneAnimations,
-		RoomPlayerState &playerState);
-	static bool startDeathAnimation(RoomPlayerState &playerState, int damageType, bool goreEnabled);
-	static bool updateDeathAnimationState(RoomPlayerState &playerState);
-	static bool startTurnAnimation(RoomPlayerState &playerState, int targetFacing);
-	static bool updateTurnAnimationState(RoomPlayerState &playerState);
-	static bool stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state,
-		const Common::Array<ObjectRecord> &sceneObjects,
-		const Common::Array<AnimRecord> &sceneAnimations,
-		RoomPlayerState &playerState, int regionFacing = -1);
-	static bool stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState &state,
-		const Common::Array<ObjectRecord> &sceneObjects,
-		const Common::Array<AnimRecord> &sceneAnimations,
-		RoomPlayerState &playerState, bool moveLeft, bool moveRight, bool moveUp, bool moveDown);
-	static bool requestIdleAnimationExit(const RoomSetupState &state,
-		RoomPlayerState &playerState, RoomIdleAnimationState &idleState);
-	static bool startIdleAnimation(HarvesterEngine &engine, const RoomSetupState &state,
-		RoomPlayerState &playerState, RoomIdleAnimationState &idleState);
-	static bool updateIdleAnimation(const RoomSetupState &state, RoomPlayerState &playerState,
-		RoomIdleAnimationState &idleState);
+	void syncCombatLoadout(HarvesterEngine &engine, const RoomSetupState &state);
+	bool syncCombatLoadoutVisual(HarvesterEngine &engine, const RoomSetupState &state, int loadout);
+	bool setIdleAnimation(int facing);
+	bool startAttackAnimation(const RoomSetupState &state, const Common::Point &mousePos);
+	bool startKeyboardAttackAnimation(const RoomSetupState &state,
+		bool attackLeft, bool attackRight, bool attackUp, bool attackDown);
+	bool updateAttackAnimationState(HarvesterEngine &engine);
+	bool startHitAnimation(HarvesterEngine &engine, int monsterAttackFirstFrame);
+	bool updateHitAnimationState(HarvesterEngine &engine, const RoomSetupState &state,
+		const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations);
+	bool startDeathAnimation(int damageType, bool goreEnabled);
+	bool updateDeathAnimationState();
+	bool startTurnAnimation(int targetFacing);
+	bool updateTurnAnimationState();
+	bool stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state,
+		const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations,
+		int regionFacing = -1);
+	bool stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState &state,
+		const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations,
+		bool moveLeft, bool moveRight, bool moveUp, bool moveDown);
+	bool requestIdleAnimationExit(const RoomSetupState &state);
+	bool startIdleAnimation(HarvesterEngine &engine, const RoomSetupState &state);
+	bool updateIdleAnimation(const RoomSetupState &state);
+
+private:
+	RoomPlayerState _state;
+	RoomIdleAnimationState _idleState;
 };
 
 } // End of namespace Harvester
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index 8a3cb38e265..9ba5b049b58 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -52,58 +52,6 @@ struct SceneState {
 	float targetPaletteBrightness = 1.0f;
 };
 
-struct RoomPlayerState {
-	Entity *entity = nullptr;
-	int centerX = 0;
-	int bottomY = 0;
-	float z = 0.0f;
-	int facing = -1;
-	int combatLoadout = 0;
-	uint32 nextMovementTick = 0;
-	int keyboardWalkRequest = -1;
-	int walkHorizontalDirection = 0;
-	int walkDepthDirection = 0;
-	bool hasMoveTarget = false;
-	int targetX = 0;
-	int targetBottomY = 0;
-	float targetZ = 0.0f;
-	bool moveTargetXReached = false;
-	bool moveTargetZReached = false;
-	Common::String currentBlockerName;
-
-	Common::String upwardBlockerHistory;
-	Common::String downwardBlockerHistory;
-	Common::String leftwardBlockerHistory;
-	Common::String rightwardBlockerHistory;
-	bool turnActive = false;
-	int turnTargetFacing = -1;
-	int turnFirstFrame = -1;
-	int turnLastFrame = -1;
-	int turnEndFrame = -1;
-	bool turnPlayBackwards = false;
-	bool attackActive = false;
-	int attackFirstFrame = -1;
-	int attackLastFrame = -1;
-	int attackContactFrame = -1;
-	int attackResumeFacing = -1;
-	bool attackSoundPlayed = false;
-	int attackSoundPlaybackFrame = -1;
-	bool attackContactResolved = false;
-
-	Common::String attackTargetName;
-	int attackTargetClassId = -1;
-	bool hitActive = false;
-	int hitFirstFrame = -1;
-	int hitLastFrame = -1;
-	int hitResumeFacing = -1;
-	int hitKnockbackRemainingX = 0;
-	int hitKnockbackDecayStep = 0;
-	bool deathActive = false;
-	int deathFirstFrame = -1;
-	int deathLastFrame = -1;
-	int deathDamageType = 0;
-};
-
 struct RoomHoverState {
 	const ObjectRecord *object = nullptr;
 	const NpcRecord *npc = nullptr;
@@ -113,17 +61,6 @@ struct RoomHoverState {
 	int cursorSequence = 7;
 };
 
-struct RoomIdleAnimationState {
-	Entity *entity = nullptr;
-	int restoreFacing = -1;
-	uint32 activityTick = 0;
-	uint32 resetTick = 0;
-	uint32 triggerTick = 0;
-	bool active = false;
-	bool loopStarted = false;
-	bool exiting = false;
-};
-
 bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs);
 bool applyRoomActorPlacement(const RoomSetupState &state, Entity &entity,
 	int centerX, int bottomY, float z, const Common::String *entranceName = nullptr);
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index 63a04825872..9e938eab48a 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -128,78 +128,57 @@ Scene::Scene(HarvesterEngine &engine, Flow &flow, SceneState &scene, EntityManag
 	  _cheats(cheats), _view(view), _canExitCloseupToParent(canExitCloseupToParent),
 	  _roomItems(engine, scene, _sceneRuntime, inventory, _pendingTransition.roomName),
 	  _presentation(engine, scene, view.art, view.inspectFont, view.useNativeInspectFont),
-	  _interactionProcessor(engine, flow, scene, _playerState, _pendingRegionName, _pendingTransition.roomName,
+	  _interactionProcessor(engine, flow, scene, _player, _pendingRegionName, _pendingTransition.roomName,
 		_pendingTransition.isRoomName, _pendingTransition.usesSavedRoomState, _pendingTransition.savedRoomState,
 		canExitCloseupToParent, *this) {
 }
 
 void Scene::resetIdleState() {
-	_idleState = RoomIdleAnimationState();
-	_idleState.activityTick = Player::getRuntimeClockTicks();
-	_idleState.resetTick = _idleState.activityTick;
-	Player::updateIdleTrigger(_idleState);
+	_player.resetIdleState();
 }
 
 void Scene::notePlayerActivity() {
-	_idleState.activityTick = Player::getRuntimeClockTicks();
-	Player::updateIdleTrigger(_idleState);
+	_player.noteActivity();
 }
 
 bool Scene::isPlayerCombatLocked() const {
-	return _playerState.attackActive || _playerState.hitActive || _playerState.deathActive;
+	return _player.isCombatLocked();
 }
 
 bool Scene::cancelPlayerAttackAnimation() {
-	if (!_playerState.attackActive)
-		return false;
-
-	_playerState.attackActive = false;
-	_playerState.attackFirstFrame = -1;
-	_playerState.attackLastFrame = -1;
-	_playerState.attackContactFrame = -1;
-	if (!_playerState.entity)
-		return false;
-	_playerState.attackSoundPlayed = false;
-	_playerState.attackSoundPlaybackFrame = -1;
-	_playerState.attackContactResolved = false;
-	_playerState.attackTargetName.clear();
-	_playerState.attackTargetClassId = -1;
-	const int resumeFacing = _playerState.attackResumeFacing >= 0
-		? _playerState.attackResumeFacing
-		: (_playerState.facing >= 0 ? _playerState.facing : 0);
-	_playerState.attackResumeFacing = -1;
-	return Player::setIdleAnimation(_playerState, resumeFacing);
+	return _player.cancelAttackAnimation();
 }
 
 void Scene::stopPlayerRegionInteraction() {
+	const RoomPlayerState &playerState = _player.getState();
 	_inputState.moveLeft = false;
 	_inputState.moveRight = false;
 	_inputState.moveUp = false;
 	_inputState.moveDown = false;
 	_pendingRegionName.clear();
 	(void)cancelPlayerAttackAnimation();
-	_playerState.hasMoveTarget = false;
-	_playerState.turnActive = false;
-	_playerState.turnTargetFacing = -1;
-	if (_playerState.entity && _playerState.facing >= 0)
-		(void)Player::setIdleAnimation(_playerState, _playerState.facing);
+	_player.cancelMovement();
+	if (playerState.entity && playerState.facing >= 0)
+		(void)_player.setIdleAnimation(playerState.facing);
 	syncPlayerPlacement();
 }
 
 void Scene::syncPlayerPlacement() {
-	Player::syncAnimationPlacement(_playerState);
+	_player.syncAnimationPlacement();
 }
 
 void Scene::captureCurrentSaveState() {
+	const RoomPlayerState &playerState = _player.getState();
 	syncPlayerPlacement();
 	_sceneRuntime.syncCurrentRoomRuntimeState(_scene.sceneRegions);
-	const int facing = _playerState.facing >= 0 ? _playerState.facing : _scene.state.playerFacing;
+	const int facing = playerState.facing >= 0 ? playerState.facing : _scene.state.playerFacing;
 	_engine.captureCurrentSaveRoomState(_scene.state.entranceName, _scene.state.roomName,
-		_playerState.centerX, _playerState.bottomY, (int)_playerState.z, facing,
+		playerState.centerX, playerState.bottomY, (int)playerState.z, facing,
 		_scene.state.discNumber, _engine.getMusicPath());
 }
 
 bool Scene::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) {
+	const RoomPlayerState &playerState = _player.getState();
 	Script *script = _engine.getScript();
 	ResourceManager *resources = _engine.getResources();
 	if (!script || !resources)
@@ -234,9 +213,8 @@ bool Scene::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) {
 	_scene.sceneRegions = updatedScene.sceneRegions;
 	_roomItems.hideCarriedRoomItem();
 	const int playerCombatLoadout = script->getPlayerCombatLoadout();
-	if (_playerState.entity && _playerState.combatLoadout != playerCombatLoadout &&
-			!Player::syncCombatLoadoutVisual(
-				_engine, updatedState, _playerState, playerCombatLoadout)) {
+	if (playerState.entity && playerState.combatLoadout != playerCombatLoadout &&
+			!_player.syncCombatLoadoutVisual(_engine, updatedState, playerCombatLoadout)) {
 		return false;
 	}
 	if (preservePaletteState) {
@@ -268,9 +246,7 @@ bool Scene::refreshCurrentScene(bool preservePlayerPlacement) {
 		return false;
 	_sceneRuntime.restoreRoomMonsterAnimations();
 	_roomItems.hideCarriedRoomItem();
-	Player::rebindAfterSceneRefresh(_scene.state, _playerState, _entityManager, preservePlayerPlacement);
-	_playerState.combatLoadout =
-		_engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0;
+	_player.rebindAfterSceneRefresh(_engine, _scene.state, _entityManager, preservePlayerPlacement);
 	_combat.resetForSceneRefresh(_scene.state);
 	_pendingRegionName.clear();
 	resetIdleState();
@@ -280,20 +256,14 @@ bool Scene::refreshCurrentScene(bool preservePlayerPlacement) {
 }
 
 void Scene::initializePlayerState() {
-	_playerState.entity = _entityManager ? _entityManager->findSceneEntityByName("PLAYER") : nullptr;
-	_playerState.centerX = _scene.state.playerSpawnX;
-	_playerState.bottomY = _scene.state.playerSpawnY;
-	_playerState.z = (float)_scene.state.playerSpawnZ;
-	_playerState.facing = _scene.state.playerFacing;
-	_playerState.combatLoadout =
-		_engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0;
-	_playerState.turnActive = false;
-	_playerState.turnTargetFacing = -1;
+	_player.initialize(_scene.state,
+		_entityManager ? _entityManager->findSceneEntityByName("PLAYER") : nullptr,
+		_engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0);
 	_combat.initializeCombatStates(_scene.state);
 }
 
 void Scene::syncAnimatedRoomActorPlacement() {
-	Player::syncAnimationPlacement(_playerState);
+	_player.syncAnimationPlacement();
 	if (!_entityManager)
 		return;
 
@@ -360,31 +330,7 @@ Common::Error Scene::runRoomExitCommands() {
 }
 
 void Scene::applyPlayerGotoXZ(int x, int z) {
-	_playerState.hasMoveTarget = false;
-	_playerState.turnActive = false;
-	_playerState.turnTargetFacing = -1;
-	_playerState.centerX = x;
-	_playerState.z = (float)z;
-	_playerState.bottomY = mapRoomDepthToScreenYForCombat(
-		_scene.state, _playerState.z, _playerState.bottomY);
-	Common::Rect screenRect;
-	bool hasScreenRect = false;
-	float depthScale = 0.0f;
-	if (_playerState.entity) {
-		(void)applyRoomActorPlacement(_scene.state, *_playerState.entity,
-			_playerState.centerX, _playerState.bottomY, _playerState.z);
-		if (_entityManager)
-			_entityManager->reinsertSceneEntity(_playerState.entity);
-		screenRect = _playerState.entity->getScreenRect();
-		hasScreenRect = true;
-		depthScale = _playerState.entity->getDepthScale();
-	}
-	debugC(1, kDebugPlayer,
-		"Harvester: PC_GOTO_XZ applied x=%d z=%d pos=(%d,%d,z=%.2f) screen_rect=(%d,%d)-(%d,%d) depth_scale=%.3f",
-		x, z, _playerState.centerX, _playerState.bottomY, (double)_playerState.z,
-		hasScreenRect ? screenRect.left : 0, hasScreenRect ? screenRect.top : 0,
-		hasScreenRect ? screenRect.right : 0, hasScreenRect ? screenRect.bottom : 0,
-		(double)depthScale);
+	_player.applyGotoXZ(_scene.state, _entityManager, x, z);
 	resetIdleState();
 	captureCurrentSaveState();
 }
@@ -403,21 +349,13 @@ void Scene::requestPlayerGameOver(const char *reason, const Common::String &sour
 }
 
 void Scene::startPlayerDefeatSequence(const char *reason, const Common::String &sourceName, int damageType) {
-	if (_flow.hasPendingMainMenuReturn() || _playerState.deathActive)
+	const RoomPlayerState &playerState = _player.getState();
+	if (_flow.hasPendingMainMenuReturn() || playerState.deathActive)
 		return;
 
-	if (_idleState.entity) {
-		_idleState.entity->setVisible(false);
-		_idleState.entity->setAnimationRate(0);
-	}
-	_idleState.active = false;
-	_idleState.loopStarted = false;
-	_idleState.exiting = false;
-	_idleState.restoreFacing = -1;
-	if (_playerState.entity)
-		_playerState.entity->setVisible(true);
-
-	if (!Player::startDeathAnimation(_playerState, damageType, _engine.isGoreEnabled())) {
+	_player.cancelIdleAnimation();
+
+	if (!_player.startDeathAnimation(damageType, _engine.isGoreEnabled())) {
 		debugC(1, kDebugCombat,
 			"Harvester: player defeat fallback reason='%s' source='%s' damage_type=%d hp=%d",
 			reason ? reason : "", sourceName.c_str(), damageType,
@@ -466,6 +404,8 @@ void Scene::updateRoomInterface(bool &needsRedraw) {
 }
 
 bool Scene::drawRoomFrame(bool &needsRedraw) {
+	const RoomPlayerState &playerState = _player.getState();
+	const RoomIdleAnimationState &idleState = _player.getIdleState();
 	if (!needsRedraw)
 		return true;
 
@@ -478,7 +418,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 	const bool roomCarryActive = _roomItems.hasCarriedRoomItem();
 	const bool activeCarry = inventorySelectionActive || roomCarryActive;
 	const bool inventoryPanelContainsMouse = _inventory.isOpen() && inventoryPanelBounds.contains(_flow._mousePos);
-	const bool suppressHover = _presentationState.showingInspectText || _idleState.active || _idleState.exiting ||
+	const bool suppressHover = _presentationState.showingInspectText || idleState.active || idleState.exiting ||
 		isPlayerCombatLocked() ||
 		(_inventory.isOpen() && (inventoryPanelContainsMouse || !inventorySelectionActive));
 	RoomHoverState hoverState = suppressHover
@@ -491,7 +431,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 		const Entity *target = suppressHover ? nullptr : _roomItems.findSelectedItemTarget(_flow._mousePos, false);
 		if (!target)
 			return Common::String();
-		if (target == _playerState.entity)
+		if (target == playerState.entity)
 			return Common::String(kPlayerInventoryLabel);
 		if (const ObjectRecord *object = findSceneObjectByName(_scene.sceneObjects, target->getName()))
 			return _engine.getScript()->resolveObjectLabel(*object);
@@ -539,7 +479,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 		_presentationState.currentInteractionPromptText = promptText;
 	if (Entity *cursor = _entityManager ? _entityManager->getCursorEntity() : nullptr) {
 		cursor->setAnimationSequence(
-			(_presentationState.showingInspectText || _idleState.active || _idleState.exiting ||
+			(_presentationState.showingInspectText || idleState.active || idleState.exiting ||
 				isPlayerCombatLocked() ||
 				_inventory.isOpen() || activeCarry)
 				? 7
@@ -548,7 +488,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 
 	drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
 	_combatEffects.drawCombatDamagePopups(*activeScreen, _engine.isCombatDebugEnabled());
-	if (_engine.getScript() && _playerState.entity) {
+	if (_engine.getScript() && playerState.entity) {
 		drawCombatLoadoutResourceIcons(*activeScreen, *_view.art, *_engine.getScript(),
 			_engine.getScript()->getPlayerCombatLoadout());
 	}
@@ -599,13 +539,14 @@ InputResult Scene::pollRoomEvents(Input &input, bool &needsRedraw) {
 }
 
 Scene::PlayerControlState Scene::preparePlayerControlState() {
+	const RoomPlayerState &playerState = _player.getState();
 	Script *script = _engine.getScript();
 	const bool playerAlive = script && script->getPlayerCurrentHitPoints() > 0;
 	const bool playerControlPaused = script && script->isPlayerControlPaused();
 	const bool playerCanAct = playerAlive && !playerControlPaused;
 	if (!playerAlive && (_inputState.moveLeft || _inputState.moveRight || _inputState.moveUp || _inputState.moveDown ||
-			_playerState.hasMoveTarget || _playerState.turnActive ||
-			_playerState.attackActive || _playerState.hitActive)) {
+			playerState.hasMoveTarget || playerState.turnActive ||
+			playerState.attackActive || playerState.hitActive)) {
 		_inputState.attackModifierHeld = false;
 		stopPlayerRegionInteraction();
 	}
@@ -624,77 +565,76 @@ void Scene::advanceRuntimeEntities(bool &needsRedraw) {
 }
 
 void Scene::updatePlayerCombatAnimations(bool &needsRedraw) {
-	if (Player::updateAttackAnimationState(_engine, _playerState))
+	if (_player.updateAttackAnimationState(_engine))
 		needsRedraw = true;
-	if (Player::updateHitAnimationState(
-			_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations, _playerState)) {
+	if (_player.updateHitAnimationState(_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations)) {
 		needsRedraw = true;
 	}
 }
 
 void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsRedraw) {
+	const RoomPlayerState &playerState = _player.getState();
+	const RoomIdleAnimationState &idleState = _player.getIdleState();
 	const bool keyboardAttackRequested =
 		_inputState.attackModifierHeld && (_inputState.moveLeft || _inputState.moveRight || _inputState.moveUp || _inputState.moveDown);
 	if (control.canAct &&
-			!_playerState.attackActive && !_playerState.hitActive && keyboardAttackRequested &&
-			!_idleState.active && !_idleState.exiting) {
+			!playerState.attackActive && !playerState.hitActive && keyboardAttackRequested &&
+			!idleState.active && !idleState.exiting) {
 		const uint32 now = Player::getRuntimeClockTicks();
 		if (_inputState.nextKeyboardAttackAllowedTick == 0 || (int32)(now - _inputState.nextKeyboardAttackAllowedTick) >= 0) {
-			_combat.capturePlayerAttackTargetForKeyboard(_scene.state, _playerState, _entityManager, _inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown);
-			if (Player::startKeyboardAttackAnimation(
-					_scene.state, _playerState, _inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown)) {
+			_combat.capturePlayerAttackTargetForKeyboard(_scene.state, _player, _entityManager,
+				_inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown);
+			if (_player.startKeyboardAttackAnimation(_scene.state,
+					_inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown)) {
 				_inputState.nextKeyboardAttackAllowedTick = now + kNativeKeyboardAttackRepeatTicks;
 				notePlayerActivity();
 				debugC(1, kDebugCombat,
 					"Harvester: _combat player keyboard attack start loadout=%d weapon='%s' damage=%d damage_type='%s' target_class='%s' target='%s' input=(L=%d R=%d U=%d D=%d) facing=%d",
-					_playerState.combatLoadout,
-					Player::describeCombatLoadout(_playerState.combatLoadout),
-					Player::resolveCombatLoadoutDamageAmount(_playerState.combatLoadout),
-					Player::describeCombatDamageType(Player::resolveCombatLoadoutDamageType(_playerState.combatLoadout)),
-					describeCombatTargetClass(_playerState.attackTargetClassId),
-					_playerState.attackTargetName.c_str(),
-					_inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown, _playerState.facing);
+					playerState.combatLoadout,
+					Player::describeCombatLoadout(playerState.combatLoadout),
+					Player::resolveCombatLoadoutDamageAmount(playerState.combatLoadout),
+					Player::describeCombatDamageType(Player::resolveCombatLoadoutDamageType(playerState.combatLoadout)),
+					describeCombatTargetClass(playerState.attackTargetClassId),
+					playerState.attackTargetName.c_str(),
+					_inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown, playerState.facing);
 				needsRedraw = true;
 			} else {
-				_playerState.attackTargetName.clear();
-				_playerState.attackTargetClassId = -1;
+				_player.clearAttackTarget();
 			}
 		}
 	}
-	if (control.canAct && !_playerState.attackActive && !_playerState.hitActive &&
-			Player::updateTurnAnimationState(_playerState)) {
+	if (control.canAct && !playerState.attackActive && !playerState.hitActive &&
+			_player.updateTurnAnimationState()) {
 		needsRedraw = true;
 	}
 
 	if (control.canAct &&
-			!_playerState.attackActive && !_playerState.hitActive &&
-			!keyboardAttackRequested && !_idleState.active && !_idleState.exiting) {
+			!playerState.attackActive && !playerState.hitActive &&
+			!keyboardAttackRequested && !idleState.active && !idleState.exiting) {
 		const int regionFacing = _interactionProcessor.getPendingRegionFacing();
-		if (Player::stepKeyboardMovement(_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations,
-				_playerState, _inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown)) {
+		if (_player.stepKeyboardMovement(_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations,
+				_inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown)) {
 			notePlayerActivity();
 			needsRedraw = true;
-		} else if (Player::stepMoveTarget(
-				_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations,
-				_playerState, regionFacing)) {
+		} else if (_player.stepMoveTarget(_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations, regionFacing)) {
 			notePlayerActivity();
 			needsRedraw = true;
 		}
 
 		if (!_presentationState.showingInspectText && !keyboardAttackRequested &&
 				!_inputState.moveLeft && !_inputState.moveRight && !_inputState.moveUp && !_inputState.moveDown &&
-				!_playerState.hasMoveTarget && !_playerState.turnActive && !_playerState.hitActive &&
-				_playerState.entity && _playerState.facing >= 0 &&
+				!playerState.hasMoveTarget && !playerState.turnActive && !playerState.hitActive &&
+				playerState.entity && playerState.facing >= 0 &&
 				!Player::isIdleAnimationExcludedRoom(_scene.state.roomName) &&
-				Player::getRuntimeClockTicks() > _idleState.triggerTick &&
-				Player::startIdleAnimation(_engine, _scene.state, _playerState, _idleState)) {
+				Player::getRuntimeClockTicks() > idleState.triggerTick &&
+				_player.startIdleAnimation(_engine, _scene.state)) {
 			needsRedraw = true;
 		}
 	}
 }
 
 void Scene::updatePlayerDeath(bool &needsRedraw) {
-	if (Player::updateDeathAnimationState(_playerState)) {
+	if (_player.updateDeathAnimationState()) {
 		requestPlayerGameOver("combat_player_death_complete", Common::String());
 		needsRedraw = true;
 	}
@@ -924,13 +864,13 @@ Common::Error Scene::tryActivatePassiveRegion(bool &needsRedraw) {
 }
 
 Common::Error Scene::resolvePlayerAttackContact(bool &needsRedraw) {
-	return _combat.resolvePlayerAttackContact(_engine, _scene.state, _playerState, _entityManager,
+	return _combat.resolvePlayerAttackContact(_engine, _scene.state, _player, _entityManager,
 		_sceneRuntime, _combatEffects, _interactionProcessor, needsRedraw);
 }
 
 Common::Error Scene::updateRoomMonsterCombat(bool &needsRedraw) {
-	return _combat.updateRoomMonsterCombat(_engine, _scene.state, _playerState, _idleState,
-		_entityManager, _sceneRuntime, _combatEffects, _cheats, *this, _interactionProcessor, needsRedraw);
+	return _combat.updateRoomMonsterCombat(_engine, _scene.state, _player, _entityManager,
+		_sceneRuntime, _combatEffects, _cheats, *this, _interactionProcessor, needsRedraw);
 }
 
 void Scene::syncCombatHitEffects(bool &needsRedraw) {
@@ -939,16 +879,16 @@ void Scene::syncCombatHitEffects(bool &needsRedraw) {
 }
 
 void Scene::updateIdleAnimation(bool &needsRedraw) {
-	if (Player::updateIdleAnimation(_scene.state, _playerState, _idleState))
+	if (_player.updateIdleAnimation(_scene.state))
 		needsRedraw = true;
 }
 
 void Scene::capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *npc) {
-	_combat.capturePlayerAttackTarget(_scene.state, _playerState, _entityManager, mousePos, npc);
+	_combat.capturePlayerAttackTarget(_scene.state, _player, _entityManager, mousePos, npc);
 }
 
 Common::Error Scene::killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw) {
-	return _combat.killDebugActiveMonster(_engine, _scene.state, _playerState, _entityManager,
+	return _combat.killDebugActiveMonster(_engine, _scene.state, _player, _entityManager,
 		mousePos, _combatEffects, _interactionProcessor, needsRedraw);
 }
 
diff --git a/engines/harvester/scene.h b/engines/harvester/scene.h
index 8c896f14bf7..733a276743e 100644
--- a/engines/harvester/scene.h
+++ b/engines/harvester/scene.h
@@ -26,6 +26,7 @@
 #include "common/str.h"
 #include "harvester/combat.h"
 #include "harvester/input.h"
+#include "harvester/player.h"
 #include "harvester/interaction.h"
 #include "harvester/room_presentation.h"
 #include "harvester/scene_entities.h"
@@ -63,8 +64,7 @@ public:
 	Scene(HarvesterEngine &engine, Flow &flow, SceneState &scene, EntityManager *entityManager,
 		InventorySystem &inventory, CheatSystem &cheats, const RoomViewResources &view, bool canExitCloseupToParent);
 
-	RoomPlayerState &getPlayerState() { return _playerState; }
-	RoomIdleAnimationState &getIdleState() { return _idleState; }
+	Player &getPlayer() { return _player; }
 	InputState &getInputState() { return _inputState; }
 
 	void initializePlayerState();
@@ -125,10 +125,9 @@ private:
 	EntityManager *_entityManager;
 	InventorySystem &_inventory;
 	SceneEntities _sceneRuntime;
-	RoomPlayerState _playerState;
+	Player _player;
 	Combat _combat;
 	CombatEffects _combatEffects;
-	RoomIdleAnimationState _idleState;
 	InputState _inputState;
 	Common::String _pendingRegionName;
 	CheatSystem &_cheats;


Commit: 852752b9533ad50e34006aac0083b64e74a46457
    https://github.com/scummvm/scummvm/commit/852752b9533ad50e34006aac0083b64e74a46457
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:06-04:00

Commit Message:
HARVESTER: improve Combat design

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/combat.cpp
    engines/harvester/combat.h
    engines/harvester/scene.cpp
    engines/harvester/scene.h


diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat.cpp
index ee6e1325fa4..8c59eb59662 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -597,15 +597,23 @@ const char *describeCombatTargetClass(int classId) {
 	}
 }
 
-MonsterRecord *Combat::findMonsterTargetAtPoint(RoomSetupState &state, EntityManager *entityManager, const Common::Point &point) const {
+Combat::Combat(HarvesterEngine &engine, SceneState &scene, Player &player,
+		EntityManager *entityManager, SceneEntities &sceneRuntime, CombatEffects &combatEffects,
+		const CheatSystem &cheats, InteractionCallbacks &callbacks, Interaction &interactionProcessor)
+	: _engine(engine), _scene(scene), _player(player), _entityManager(entityManager),
+	  _sceneRuntime(sceneRuntime), _combatEffects(combatEffects), _cheats(cheats),
+	  _callbacks(callbacks), _interactionProcessor(interactionProcessor) {
+}
+
+MonsterRecord *Combat::findMonsterTargetAtPoint(const Common::Point &point) const {
 	MonsterRecord *bestMonster = nullptr;
 	int bestZ = -0x7fffffff;
 	int bestTop = -0x7fffffff;
-	for (MonsterRecord &monster : state.roomMonsters) {
+	for (MonsterRecord &monster : _scene.state.roomMonsters) {
 		if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
 			continue;
 
-		Entity *entity = findSceneRuntimeEntity(entityManager, monster.monsterName);
+		Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
 		if (!entity || !entity->isVisible())
 			continue;
 
@@ -624,17 +632,17 @@ MonsterRecord *Combat::findMonsterTargetAtPoint(RoomSetupState &state, EntityMan
 	return bestMonster;
 }
 
-MonsterRecord *Combat::findOverlappingMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
-		EntityManager *entityManager) const {
+MonsterRecord *Combat::findOverlappingMonsterTarget() const {
+	const RoomPlayerState &playerState = _player.getState();
 	if (!playerState.entity)
 		return nullptr;
 
-	for (MonsterRecord &monster : state.roomMonsters) {
+	for (MonsterRecord &monster : _scene.state.roomMonsters) {
 		if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
 			continue;
 
-		Entity *entity = findSceneRuntimeEntity(entityManager, monster.monsterName);
-		if (entity && areCombatantsWithinRoomCombatReach(state,
+		Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
+		if (entity && areCombatantsWithinRoomCombatReach(_scene.state,
 				*playerState.entity, playerState.z, *entity, (float)monster.posZ, monster.engageDistance))
 			return &monster;
 	}
@@ -642,16 +650,17 @@ MonsterRecord *Combat::findOverlappingMonsterTarget(RoomSetupState &state, const
 	return nullptr;
 }
 
-NpcRecord *Combat::findOverlappingNpcTarget(RoomSetupState &state, const RoomPlayerState &playerState, EntityManager *entityManager) const {
+NpcRecord *Combat::findOverlappingNpcTarget() const {
+	const RoomPlayerState &playerState = _player.getState();
 	if (!playerState.entity)
 		return nullptr;
 
-	for (NpcRecord &npc : state.roomNpcs) {
+	for (NpcRecord &npc : _scene.state.roomNpcs) {
 		if (!npc.visible || npc.deathOrMonsterfyFlag || npc.deathDamageType != 0)
 			continue;
 
-		Entity *entity = findSceneRuntimeEntity(entityManager, npc.npcName);
-		if (entity && areCombatantsWithinRoomCombatReach(state,
+		Entity *entity = findSceneRuntimeEntity(_entityManager, npc.npcName);
+		if (entity && areCombatantsWithinRoomCombatReach(_scene.state,
 				*playerState.entity, playerState.z, *entity, entity->getZ(), 0))
 			return &npc;
 	}
@@ -659,19 +668,19 @@ NpcRecord *Combat::findOverlappingNpcTarget(RoomSetupState &state, const RoomPla
 	return nullptr;
 }
 
-bool Combat::playerAttackCanReachTarget(RoomSetupState &state, const RoomPlayerState &playerState,
-		Entity *targetEntity, int engageDistance) const {
+bool Combat::playerAttackCanReachTarget(Entity *targetEntity, int engageDistance) const {
+	const RoomPlayerState &playerState = _player.getState();
 	if (!playerState.entity || !targetEntity)
 		return false;
 	if (Player::isProjectileCombatLoadout(playerState.combatLoadout))
 		return true;
 
-	return areCombatantsWithinRoomCombatReach(state,
+	return areCombatantsWithinRoomCombatReach(_scene.state,
 		*playerState.entity, playerState.z, *targetEntity, targetEntity->getZ(), engageDistance);
 }
 
-MonsterRecord *Combat::findProjectileContactMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
-		EntityManager *entityManager) const {
+MonsterRecord *Combat::findProjectileContactMonsterTarget() const {
+	const RoomPlayerState &playerState = _player.getState();
 	if (!playerState.entity)
 		return nullptr;
 
@@ -688,11 +697,11 @@ MonsterRecord *Combat::findProjectileContactMonsterTarget(RoomSetupState &state,
 	Entity *bestEntity = nullptr;
 	int bestLeadingEdge = attackSide == kNativeAttackSideLeft ? -0x7fffffff : 0x7fffffff;
 
-	for (MonsterRecord &monster : state.roomMonsters) {
+	for (MonsterRecord &monster : _scene.state.roomMonsters) {
 		if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
 			continue;
 
-		Entity *entity = findSceneRuntimeEntity(entityManager, monster.monsterName);
+		Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
 		if (!entity || !entity->isVisible() ||
 				!doRuntimeEntityDepthExtentsOverlap(*playerState.entity, *entity)) {
 			continue;
@@ -737,10 +746,9 @@ MonsterRecord *Combat::findProjectileContactMonsterTarget(RoomSetupState &state,
 	return bestMonster;
 }
 
-void Combat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, Player &player,
-		EntityManager *entityManager, bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const {
-	const RoomPlayerState &playerState = player.getState();
-	player.clearAttackTarget();
+void Combat::capturePlayerAttackTargetForKeyboard(bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const {
+	const RoomPlayerState &playerState = _player.getState();
+	_player.clearAttackTarget();
 	if (!playerState.entity)
 		return;
 
@@ -820,15 +828,15 @@ void Combat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, Player
 		bestLeadingEdge = targetLeft;
 	};
 
-	for (NpcRecord &npc : state.roomNpcs) {
+	for (NpcRecord &npc : _scene.state.roomNpcs) {
 		if (!npc.visible || npc.deathOrMonsterfyFlag || npc.deathDamageType != 0)
 			continue;
-		considerTarget(findSceneRuntimeEntity(entityManager, npc.npcName), npc.npcName, kEntityClassNpc);
+		considerTarget(findSceneRuntimeEntity(_entityManager, npc.npcName), npc.npcName, kEntityClassNpc);
 	}
-	for (MonsterRecord &monster : state.roomMonsters) {
+	for (MonsterRecord &monster : _scene.state.roomMonsters) {
 		if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
 			continue;
-		considerTarget(findSceneRuntimeEntity(entityManager, monster.monsterName), monster.monsterName, kEntityClassMonster);
+		considerTarget(findSceneRuntimeEntity(_entityManager, monster.monsterName), monster.monsterName, kEntityClassMonster);
 	}
 
 	if (!bestEntity) {
@@ -838,7 +846,7 @@ void Combat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, Player
 		return;
 	}
 
-	player.captureAttackTarget(bestName, bestClassId);
+	_player.captureAttackTarget(bestName, bestClassId);
 	debugC(1, kDebugCombat,
 		"Harvester: combat player keyboard target capture class='%s' name='%s' input=(L=%d R=%d U=%d D=%d) facing=%d target_rect=(%d,%d)-(%d,%d)",
 		describeCombatTargetClass(bestClassId), bestName.c_str(),
@@ -847,24 +855,24 @@ void Combat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, Player
 		bestEntity->getScreenRect().right, bestEntity->getScreenRect().bottom);
 }
 
-MonsterRecord *Combat::resolveKillTarget(RoomSetupState &state, const RoomPlayerState &playerState,
-		EntityManager *entityManager, const Common::Point &mousePos) const {
+MonsterRecord *Combat::resolveKillTarget(const Common::Point &mousePos) const {
+	const RoomPlayerState &playerState = _player.getState();
 	if (playerState.attackTargetClassId == kEntityClassMonster) {
-		MonsterRecord *monster = findRoomMonsterRecordByName(state, playerState.attackTargetName);
-		if (isKillableMonster(monster) && selectMonsterEntity(entityManager, *monster))
+		MonsterRecord *monster = findRoomMonsterRecordByName(_scene.state, playerState.attackTargetName);
+		if (isKillableMonster(monster) && selectMonsterEntity(_entityManager, *monster))
 			return monster;
 	}
 
-	if (MonsterRecord *monster = findMonsterTargetAtPoint(state, entityManager, mousePos)) {
-		if (isKillableMonster(monster) && selectMonsterEntity(entityManager, *monster))
+	if (MonsterRecord *monster = findMonsterTargetAtPoint(mousePos)) {
+		if (isKillableMonster(monster) && selectMonsterEntity(_entityManager, *monster))
 			return monster;
 	}
 
 	if (playerState.entity) {
-		for (uint i = 0; i < state.roomMonsters.size() && i < _monsterCombatStates.size(); ++i) {
-			MonsterRecord &monster = state.roomMonsters[i];
+		for (uint i = 0; i < _scene.state.roomMonsters.size() && i < _monsterCombatStates.size(); ++i) {
+			MonsterRecord &monster = _scene.state.roomMonsters[i];
 			const MonsterCombatState &combatState = _monsterCombatStates[i];
-			if (!combatState.attackActive || !isKillableMonster(&monster) || !selectMonsterEntity(entityManager, monster))
+			if (!combatState.attackActive || !isKillableMonster(&monster) || !selectMonsterEntity(_entityManager, monster))
 				continue;
 			if (!combatState.attackTargetName.empty() &&
 					playerState.entity->getName().equalsIgnoreCase(combatState.attackTargetName)) {
@@ -880,11 +888,11 @@ MonsterRecord *Combat::resolveKillTarget(RoomSetupState &state, const RoomPlayer
 	const int playerCenterX = playerRect.left + playerRect.width() / 2;
 	MonsterRecord *bestMonster = nullptr;
 	int bestScore = 0x7fffffff;
-	for (MonsterRecord &monster : state.roomMonsters) {
+	for (MonsterRecord &monster : _scene.state.roomMonsters) {
 		if (!isKillableMonster(&monster))
 			continue;
 
-		Entity *entity = selectMonsterEntity(entityManager, monster);
+		Entity *entity = selectMonsterEntity(_entityManager, monster);
 		if (!entity)
 			continue;
 
@@ -901,19 +909,18 @@ MonsterRecord *Combat::resolveKillTarget(RoomSetupState &state, const RoomPlayer
 	return bestMonster;
 }
 
-void Combat::capturePlayerAttackTarget(RoomSetupState &state, Player &player,
-		EntityManager *entityManager, const Common::Point &mousePos, const NpcRecord *hoverNpc) const {
-	const RoomPlayerState &playerState = player.getState();
+void Combat::capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *hoverNpc) const {
+	const RoomPlayerState &playerState = _player.getState();
 	if (hoverNpc) {
-		player.captureAttackTarget(hoverNpc->npcName, kEntityClassNpc);
+		_player.captureAttackTarget(hoverNpc->npcName, kEntityClassNpc);
 		debugC(1, kDebugCombat,
 			"Harvester: combat player target capture class='npc' name='%s' cursor=(%d,%d)",
 			playerState.attackTargetName.c_str(), mousePos.x, mousePos.y);
 		return;
 	}
 
-	if (MonsterRecord *monster = findMonsterTargetAtPoint(state, entityManager, mousePos)) {
-		player.captureAttackTarget(monster->monsterName, kEntityClassMonster);
+	if (MonsterRecord *monster = findMonsterTargetAtPoint(mousePos)) {
+		_player.captureAttackTarget(monster->monsterName, kEntityClassMonster);
 		debugC(1, kDebugCombat,
 			"Harvester: combat player target capture class='monster' name='%s' cursor=(%d,%d)",
 			playerState.attackTargetName.c_str(), mousePos.x, mousePos.y);
@@ -925,62 +932,59 @@ void Combat::capturePlayerAttackTarget(RoomSetupState &state, Player &player,
 		mousePos.x, mousePos.y);
 }
 
-void Combat::captureOverlappingNpcAttackTarget(RoomSetupState &state, Player &player, EntityManager *entityManager) const {
-	const RoomPlayerState &playerState = player.getState();
+void Combat::captureOverlappingNpcAttackTarget() const {
+	const RoomPlayerState &playerState = _player.getState();
 	if (playerState.attackTargetClassId < 0 && !Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
-		if (NpcRecord *fallbackNpc = findOverlappingNpcTarget(state, playerState, entityManager)) {
-			player.captureAttackTarget(fallbackNpc->npcName, kEntityClassNpc);
+		if (NpcRecord *fallbackNpc = findOverlappingNpcTarget()) {
+			_player.captureAttackTarget(fallbackNpc->npcName, kEntityClassNpc);
 		}
 	}
 }
 
-NpcRecord *Combat::selectNpcAttackContactTarget(RoomSetupState &state, const RoomPlayerState &playerState,
-		EntityManager *entityManager, Entity *&npcEntity) const {
-	NpcRecord *npc = findRoomNpcRecordByName(state, playerState.attackTargetName);
-	npcEntity = npc ? findSceneRuntimeEntity(entityManager, npc->npcName) : nullptr;
-	if (!npc || !npcEntity || !playerAttackCanReachTarget(state, playerState, npcEntity))
-		npc = Player::isProjectileCombatLoadout(playerState.combatLoadout) ? nullptr : findOverlappingNpcTarget(state, playerState, entityManager);
-	npcEntity = npc ? findSceneRuntimeEntity(entityManager, npc->npcName) : nullptr;
+NpcRecord *Combat::selectNpcAttackContactTarget(Entity *&npcEntity) const {
+	const RoomPlayerState &playerState = _player.getState();
+	NpcRecord *npc = findRoomNpcRecordByName(_scene.state, playerState.attackTargetName);
+	npcEntity = npc ? findSceneRuntimeEntity(_entityManager, npc->npcName) : nullptr;
+	if (!npc || !npcEntity || !playerAttackCanReachTarget(npcEntity))
+		npc = Player::isProjectileCombatLoadout(playerState.combatLoadout) ? nullptr : findOverlappingNpcTarget();
+	npcEntity = npc ? findSceneRuntimeEntity(_entityManager, npc->npcName) : nullptr;
 	return npc;
 }
 
-MonsterRecord *Combat::selectMonsterAttackContactTarget(RoomSetupState &state, Player &player,
-		EntityManager *entityManager, Entity *&monsterEntity) const {
-	const RoomPlayerState &playerState = player.getState();
+MonsterRecord *Combat::selectMonsterAttackContactTarget(Entity *&monsterEntity) const {
+	const RoomPlayerState &playerState = _player.getState();
 	MonsterRecord *monster = nullptr;
 	if (playerState.attackTargetClassId == kEntityClassMonster)
-		monster = findRoomMonsterRecordByName(state, playerState.attackTargetName);
-	monsterEntity = monster ? findSceneRuntimeEntity(entityManager, monster->monsterName) : nullptr;
-	if (!monster || !monsterEntity || !playerAttackCanReachTarget(state, playerState, monsterEntity, monster->engageDistance)) {
+		monster = findRoomMonsterRecordByName(_scene.state, playerState.attackTargetName);
+	monsterEntity = monster ? findSceneRuntimeEntity(_entityManager, monster->monsterName) : nullptr;
+	if (!monster || !monsterEntity || !playerAttackCanReachTarget(monsterEntity, monster->engageDistance)) {
 		if (!Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
-			monster = findOverlappingMonsterTarget(state, playerState, entityManager);
+			monster = findOverlappingMonsterTarget();
 		} else if (playerState.attackTargetClassId < 0) {
-			monster = findProjectileContactMonsterTarget(state, playerState, entityManager);
+			monster = findProjectileContactMonsterTarget();
 			if (monster) {
-				player.captureAttackTarget(monster->monsterName, kEntityClassMonster);
+				_player.captureAttackTarget(monster->monsterName, kEntityClassMonster);
 			}
 		} else {
 			monster = nullptr;
 		}
-		monsterEntity = monster ? findSceneRuntimeEntity(entityManager, monster->monsterName) : nullptr;
+		monsterEntity = monster ? findSceneRuntimeEntity(_entityManager, monster->monsterName) : nullptr;
 	}
 	return monster;
 }
 
-NpcCombatUpdateResult Combat::updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
-		EntityManager *entityManager, SceneEntities &sceneRuntime,
-		Interaction &interactionProcessor, bool &needsRedraw) {
+NpcCombatUpdateResult Combat::updateRoomNpcCombat(bool &needsRedraw) {
 	NpcCombatUpdateResult result;
-	for (uint i = 0; i < state.roomNpcs.size(); ++i) {
-		NpcRecord &npc = state.roomNpcs[i];
+	for (uint i = 0; i < _scene.state.roomNpcs.size(); ++i) {
+		NpcRecord &npc = _scene.state.roomNpcs[i];
 		NpcCombatState &combatState = _npcCombatStates[i];
-		Entity *entity = findSceneRuntimeEntity(entityManager, npc.npcName);
+		Entity *entity = findSceneRuntimeEntity(_entityManager, npc.npcName);
 		if (!entity || !npc.visible || npc.deathOrMonsterfyFlag) {
 			clearRoomNpcCombatState(combatState);
 			continue;
 		}
 		if (!combatState.deathActive && npc.active && npc.deathDamageType != 0) {
-			if (beginNpcDeathTransition(engine, npc, *entity, combatState, npc.deathDamageType, needsRedraw))
+			if (beginNpcDeathTransition(npc, *entity, combatState, npc.deathDamageType, needsRedraw))
 				continue;
 
 			debugC(1, kDebugCombat,
@@ -989,7 +993,7 @@ NpcCombatUpdateResult Combat::updateRoomNpcCombat(HarvesterEngine &engine, RoomS
 			combatState.deathDamageType = npc.deathDamageType;
 			combatState.deathLastFrame = entity->getCurrentFrame();
 			result.finalizedDeath = true;
-			result.error = finalizeNpcDeathTransition(engine, entityManager, sceneRuntime, npc, combatState, interactionProcessor, needsRedraw);
+			result.error = finalizeNpcDeathTransition(npc, combatState, needsRedraw);
 			return result;
 		}
 		if (!combatState.deathActive)
@@ -1002,21 +1006,21 @@ NpcCombatUpdateResult Combat::updateRoomNpcCombat(HarvesterEngine &engine, RoomS
 		entity->setCurrentFrame(combatState.deathLastFrame);
 		entity->setAnimationEnabled(false);
 		result.finalizedDeath = true;
-		result.error = finalizeNpcDeathTransition(engine, entityManager, sceneRuntime, npc, combatState, interactionProcessor, needsRedraw);
+		result.error = finalizeNpcDeathTransition(npc, combatState, needsRedraw);
 		return result;
 	}
 	return result;
 }
 
-bool Combat::queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const {
+bool Combat::queueInnerSanctumHerrillDeathAfterMuck(const MonsterRecord &monster) const {
 	if (!monster.roomName.equalsIgnoreCase(kInnerSanctumRoomName) ||
 			!monster.monsterName.equalsIgnoreCase(kMuckeyMonsterName) ||
 			!monster.onDeathActionTag.equalsIgnoreCase(kActivateInnerSanctumDoorActionTag)) {
 		return false;
 	}
 
-	Script *script = engine.getScript();
-	NpcRecord *herrill = findRoomNpcRecordByName(state, kHerrillLogNpcName);
+	Script *script = _engine.getScript();
+	NpcRecord *herrill = findRoomNpcRecordByName(_scene.state, kHerrillLogNpcName);
 	if (!script || !herrill || !herrill->visible ||
 			herrill->deathOrMonsterfyFlag || herrill->deathDamageType != 0) {
 		return false;
@@ -1037,11 +1041,11 @@ bool Combat::queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, Roo
 	return true;
 }
 
-bool Combat::beginNpcDeathTransition(HarvesterEngine &engine,
-		NpcRecord &npc, Entity &npcEntity, NpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const {
+bool Combat::beginNpcDeathTransition(NpcRecord &npc, Entity &npcEntity, NpcCombatState &combatState,
+		int deathDamageType, bool &needsRedraw) const {
 	DeathAnimationRange deathRange;
 	if (!resolveNpcDeathAnimationRange(npcEntity, !npc.monsterfyTargetName.empty(),
-			deathDamageType, engine.isGoreEnabled(), deathRange))
+			deathDamageType, _engine.isGoreEnabled(), deathRange))
 		return false;
 
 	combatState.deathActive = true;
@@ -1054,7 +1058,7 @@ bool Combat::beginNpcDeathTransition(HarvesterEngine &engine,
 	npcEntity.setAnimationEnabled(true);
 	npcEntity.setCurrentFrame(deathRange.firstFrame);
 	if (!npc.audioPath.empty())
-		(void)engine.playSound(npc.audioPath);
+		(void)_engine.playSound(npc.audioPath);
 	debugC(1, kDebugCombat,
 		"Harvester: combat npc death start target='%s' damage_type=%d frames=%d..%d monsterfy='%s' on_death='%s'",
 		npc.npcName.c_str(), deathDamageType, deathRange.firstFrame, deathRange.lastFrame,
@@ -1063,11 +1067,9 @@ bool Combat::beginNpcDeathTransition(HarvesterEngine &engine,
 	return true;
 }
 
-Common::Error Combat::finalizeNpcDeathTransition(HarvesterEngine &engine,
-		EntityManager *entityManager, SceneEntities &sceneRuntime, NpcRecord &npc, NpcCombatState &combatState,
-		Interaction &interactionProcessor, bool &needsRedraw) const {
-	Script *script = engine.getScript();
-	Entity *npcEntity = findSceneRuntimeEntity(entityManager, npc.npcName);
+Common::Error Combat::finalizeNpcDeathTransition(NpcRecord &npc, NpcCombatState &combatState, bool &needsRedraw) const {
+	Script *script = _engine.getScript();
+	Entity *npcEntity = findSceneRuntimeEntity(_entityManager, npc.npcName);
 	// Native state 0x35 leaves non-monsterfied NPCs as a dead state-0x38 actor
 	// in the current room, while the NpcRecord death flag suppresses later setup.
 	const bool retainDeathEntity =
@@ -1085,7 +1087,7 @@ Common::Error Combat::finalizeNpcDeathTransition(HarvesterEngine &engine,
 		npcEntity->setHitTestMode(kEntityHitTestNone);
 		npcEntity->setVisible(true);
 	} else {
-		sceneRuntime.removeSceneEntityByName(npc.npcName);
+		_sceneRuntime.removeSceneEntityByName(npc.npcName);
 	}
 	npc.active = false;
 	npc.visible = false;
@@ -1117,24 +1119,23 @@ Common::Error Combat::finalizeNpcDeathTransition(HarvesterEngine &engine,
 		}
 	}
 
-	return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+	return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
 }
 
-void Combat::startMonsterHitReaction(HarvesterEngine &engine,
-		CombatEffects &combatEffects, MonsterRecord &monster, Entity &monsterEntity, MonsterCombatState *combatState,
+void Combat::startMonsterHitReaction(MonsterRecord &monster, Entity &monsterEntity, MonsterCombatState *combatState,
 		int attackerAttackFirstFrame, bool &needsRedraw) const {
-	if (combatEffects.spawnCombatHitEffect(monsterEntity, monster.monsterName))
+	if (_combatEffects.spawnCombatHitEffect(monsterEntity, monster.monsterName))
 		needsRedraw = true;
 
 	if (!combatState) {
-		(void)playRandomRoomAttackSound(engine,
+		(void)playRandomRoomAttackSound(_engine,
 			monster.hitSound1, monster.hitSound2, monster.hitSound3);
 		return;
 	}
 
 	HitAnimationRange range;
 	if (!resolveRoomMonsterHitAnimationRange(monsterEntity, attackerAttackFirstFrame, range)) {
-		(void)playRandomRoomAttackSound(engine,
+		(void)playRandomRoomAttackSound(_engine,
 			monster.hitSound1, monster.hitSound2, monster.hitSound3);
 		debugC(1, kDebugCombat,
 			"Harvester: combat monster hit reaction skipped target='%s' attack_frame=%d reason='no native hit bank'",
@@ -1159,7 +1160,7 @@ void Combat::startMonsterHitReaction(HarvesterEngine &engine,
 	monsterEntity.setAnimationEnabled(true);
 	monsterEntity.setCurrentFrame(range.firstFrame);
 	if (!combatState->hitSoundPlayed && monster.hitSoundTriggerFrame <= 0) {
-		(void)playRandomRoomAttackSound(engine,
+		(void)playRandomRoomAttackSound(_engine,
 			monster.hitSound1, monster.hitSound2, monster.hitSound3);
 		combatState->hitSoundPlayed = true;
 	}
@@ -1170,14 +1171,12 @@ void Combat::startMonsterHitReaction(HarvesterEngine &engine,
 	needsRedraw = true;
 }
 
-Common::Error Combat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupState &state,
-		Player &player, EntityManager *entityManager, const Common::Point &mousePos,
-		CombatEffects &combatEffects, Interaction &interactionProcessor, bool &needsRedraw) {
-	const RoomPlayerState &playerState = player.getState();
-	if (!engine.isCombatDebugEnabled())
+Common::Error Combat::killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw) {
+	const RoomPlayerState &playerState = _player.getState();
+	if (!_engine.isCombatDebugEnabled())
 		return Common::kNoError;
-	MonsterRecord *monster = resolveKillTarget(state, playerState, entityManager, mousePos);
-	Entity *monsterEntity = monster ? findSceneRuntimeEntity(entityManager, monster->monsterName) : nullptr;
+	MonsterRecord *monster = resolveKillTarget(mousePos);
+	Entity *monsterEntity = monster ? findSceneRuntimeEntity(_entityManager, monster->monsterName) : nullptr;
 	if (!monster || !monsterEntity || !monsterEntity->isVisible()) {
 		debugC(1, kDebugCombat,
 			"Harvester: debug combat kill skipped reason='no active monster' cursor=(%d,%d)",
@@ -1185,7 +1184,7 @@ Common::Error Combat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupS
 		return Common::kNoError;
 	}
 
-	Script *script = engine.getScript();
+	Script *script = _engine.getScript();
 	if (!script) {
 		debugC(1, kDebugCombat,
 			"Harvester: debug combat kill skipped target='%s' reason='no startup script'",
@@ -1197,18 +1196,18 @@ Common::Error Combat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupS
 	const int damageLanded = monster->currentHitPoints;
 	monster->currentHitPoints = 0;
 	if (damageLanded > 0)
-		combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
+		_combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
 
 	if (playerState.attackTargetClassId == kEntityClassMonster &&
 			playerState.attackTargetName.equalsIgnoreCase(monster->monsterName)) {
-		player.clearAttackTarget();
+		_player.clearAttackTarget();
 	}
 
 	debugC(1, kDebugCombat,
 		"Harvester: debug combat kill target='%s' damage=%d damage_type=%d cursor=(%d,%d)",
 		monster->monsterName.c_str(), damageLanded, debugDamageType, mousePos.x, mousePos.y);
 
-	const int monsterIndex = findRoomMonsterIndexByName(state, monster->monsterName);
+	const int monsterIndex = findRoomMonsterIndexByName(_scene.state, monster->monsterName);
 	MonsterCombatState *combatState =
 		monsterIndex >= 0 && monsterIndex < (int)_monsterCombatStates.size()
 			? &_monsterCombatStates[(uint)monsterIndex]
@@ -1219,7 +1218,7 @@ Common::Error Combat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupS
 
 	DeathAnimationRange deathRange;
 	if (combatState && resolveMonsterDeathAnimationRange(*monsterEntity, monster->facing,
-			debugDamageType, engine.isGoreEnabled(), deathRange)) {
+			debugDamageType, _engine.isGoreEnabled(), deathRange)) {
 		combatState->deathActive = true;
 		combatState->deathFirstFrame = deathRange.firstFrame;
 		combatState->deathLastFrame = deathRange.lastFrame;
@@ -1230,7 +1229,7 @@ Common::Error Combat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupS
 		monsterEntity->setAnimationEnabled(true);
 		monsterEntity->setCurrentFrame(deathRange.firstFrame);
 		if (!monster->deathSound.empty())
-			(void)engine.playSound(monster->deathSound);
+			(void)_engine.playSound(monster->deathSound);
 		needsRedraw = true;
 		return Common::kNoError;
 	}
@@ -1250,36 +1249,32 @@ Common::Error Combat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupS
 		}
 	}
 
-	return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+	return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
 }
 
-Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
-		Player &player, EntityManager *entityManager,
-		SceneEntities &sceneRuntime, CombatEffects &combatEffects, const CheatSystem &cheats,
-		InteractionCallbacks &callbacks, Interaction &interactionProcessor, bool &needsRedraw) {
-	const RoomPlayerState &playerState = player.getState();
-	if (!entityManager)
+Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
+	const RoomPlayerState &playerState = _player.getState();
+	if (!_entityManager)
 		return Common::kNoError;
 
-	Script &script = *engine.getScript();
+	Script &script = *_engine.getScript();
 	const uint32 now = Player::getRuntimeClockTicks();
 	const uint32 moveInterval = MAX<uint32>(1, 100U / (uint32)kRoomMonsterAnimationRate);
 	const int horizontalStepBase = 8;
 	const int depthStep = MAX<int>(1, roundRoomCombatFloat(
-		state.roomZVelocityStep > 0.0f ? state.roomZVelocityStep : 1.0f));
+		_scene.state.roomZVelocityStep > 0.0f ? _scene.state.roomZVelocityStep : 1.0f));
 	bool playerAlive = script.getPlayerCurrentHitPoints() > 0;
 
-	resizeCombatStates(state);
+	resizeCombatStates();
 
-	const NpcCombatUpdateResult npcResult = updateRoomNpcCombat(engine, state,
-		entityManager, sceneRuntime, interactionProcessor, needsRedraw);
+	const NpcCombatUpdateResult npcResult = updateRoomNpcCombat(needsRedraw);
 	if (npcResult.finalizedDeath)
 		return npcResult.error;
 
-	for (uint i = 0; i < state.roomMonsters.size(); ++i) {
-		MonsterRecord &monster = state.roomMonsters[i];
+	for (uint i = 0; i < _scene.state.roomMonsters.size(); ++i) {
+		MonsterRecord &monster = _scene.state.roomMonsters[i];
 		MonsterCombatState &combatState = _monsterCombatStates[i];
-		Entity *entity = findSceneRuntimeEntity(entityManager, monster.monsterName);
+		Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
 		if (!entity || !monster.visible) {
 			clearRoomMonsterCombatState(combatState);
 			continue;
@@ -1302,7 +1297,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 					monster.runtimeState,
 					monster.onDeathActionTag.c_str());
 				clearRoomMonsterCombatState(combatState);
-				const bool herrillDeathQueued = queueInnerSanctumHerrillDeathAfterMuck(engine, state, monster);
+				const bool herrillDeathQueued = queueInnerSanctumHerrillDeathAfterMuck(monster);
 
 				InteractionResult interaction;
 				interaction.mutatedRuntimeState = true;
@@ -1321,7 +1316,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 					interaction.visualRuntimeStateChanged = true;
 				}
 
-				return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+				return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
 			}
 			continue;
 		}
@@ -1341,7 +1336,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 			if (!combatState.hitSoundPlayed &&
 					monster.hitSoundTriggerFrame >= 0 &&
 					currentFrame >= combatState.hitFirstFrame + monster.hitSoundTriggerFrame) {
-				(void)playRandomRoomAttackSound(engine,
+				(void)playRandomRoomAttackSound(_engine,
 					monster.hitSound1, monster.hitSound2, monster.hitSound3);
 				combatState.hitSoundPlayed = true;
 			}
@@ -1355,7 +1350,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 					monster.posX + knockbackStep, monster.screenMinXBound, monster.screenMaxXBound);
 				if (monster.posX != previousX) {
 					(void)applyRoomActorPlacement(
-						state, *entity, monster.posX, monster.posY, (float)monster.posZ);
+						_scene.state, *entity, monster.posX, monster.posY, (float)monster.posZ);
 					(void)script.syncRuntimeMonsterRecord(monster);
 					debugC(1, kDebugCombat,
 						"Harvester: combat monster hit knockback target='%s' from_x=%d to_x=%d remaining=%d",
@@ -1379,7 +1374,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 			if (!combatState.attackSoundPlayed &&
 					monster.attackSoundTriggerFrame >= 0 &&
 					currentFrame >= combatState.attackFirstFrame + monster.attackSoundTriggerFrame) {
-				(void)playRandomRoomAttackSound(engine,
+				(void)playRandomRoomAttackSound(_engine,
 					monster.attackSound1, monster.attackSound2, monster.attackSound3);
 				combatState.attackSoundPlayed = true;
 			}
@@ -1398,7 +1393,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 						monster.monsterName.c_str(), currentFrame, script.getPlayerCurrentHitPoints());
 				} else {
 					const int playerHitPointsBefore = script.getPlayerCurrentHitPoints();
-					const bool damageDisabled = cheats.isPlayerDamageDisabled();
+					const bool damageDisabled = _cheats.isPlayerDamageDisabled();
 					const bool changed = damageDisabled
 						? false
 						: script.adjustPlayerCurrentHitPoints(-monster.damageAmount);
@@ -1411,17 +1406,17 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 						playerHitPointsBefore, playerHitPointsAfter, changed,
 						damageDisabled ? 1 : 0);
 					if (damageLanded > 0 && playerState.entity) {
-						combatEffects.spawnCombatDamagePopup(*playerState.entity, playerState.entity->getName(), damageLanded);
-						player.cancelIdleAnimation();
+						_combatEffects.spawnCombatDamagePopup(*playerState.entity, playerState.entity->getName(), damageLanded);
+						_player.cancelIdleAnimation();
 						if (playerHitPointsAfter > 0 &&
-								player.startHitAnimation(engine, combatState.attackFirstFrame)) {
-							callbacks.notePlayerActivity();
+								_player.startHitAnimation(_engine, combatState.attackFirstFrame)) {
+							_callbacks.notePlayerActivity();
 						}
 					}
 					if (changed && playerHitPointsAfter <= 0) {
 						playerAlive = false;
-						callbacks.stopPlayerRegionInteraction();
-						callbacks.startPlayerDefeatSequence(
+						_callbacks.stopPlayerRegionInteraction();
+						_callbacks.startPlayerDefeatSequence(
 							"combat_monster_hit", monster.monsterName, monster.damageType);
 						return Common::kNoError;
 					}
@@ -1462,15 +1457,15 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 		const float absZDelta = zDelta >= 0.0f ? zDelta : -zDelta;
 		const int engageDistance = MAX(0, monster.engageDistance);
 		const int liveWaypointTolerance = roundRoomCombatFloat(
-			Player::computeDepthScale(state,
+			Player::computeDepthScale(_scene.state,
 				MIN<float>(playerState.z, (float)monster.posZ)) * kNativeMonsterHorizontalWaypointTolerance);
 		bool attemptedMoveX = false;
 		bool attemptedMoveZ = false;
 		if (absZDelta > kNativeMonsterPursuitZTolerance) {
 			attemptedMoveZ = true;
 			monster.posZ = stepTowardsRoomCombatInt(monster.posZ, playerTargetZ, depthStep);
-			monster.posZ = (int)clampRoomDepthForEvent(state, (float)monster.posZ);
-			monster.posY = mapRoomDepthToScreenYForCombat(state, (float)monster.posZ, monster.posY);
+			monster.posZ = (int)clampRoomDepthForEvent(_scene.state, (float)monster.posZ);
+			monster.posY = mapRoomDepthToScreenYForCombat(_scene.state, (float)monster.posZ, monster.posY);
 		} else if (absLiveCenterDx > engageDistance) {
 			// Native class-6 pursuit stores a target frame-left X, not a center X.
 			const int desiredFrameLeftX = monsterCenterX < playerCenterX
@@ -1479,7 +1474,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 			if (ABS(monsterRect.left - desiredFrameLeftX) > liveWaypointTolerance) {
 				attemptedMoveX = true;
 				const int step = MAX<int>(1, roundRoomCombatFloat(
-					Player::computeDepthScale(state, (float)monster.posZ) * (float)horizontalStepBase));
+					Player::computeDepthScale(_scene.state, (float)monster.posZ) * (float)horizontalStepBase));
 				const int monsterFrameLeftOffset = monsterRect.left - monster.posX;
 				const int desiredLogicalCenterX = desiredFrameLeftX - monsterFrameLeftOffset;
 				monster.posX = stepTowardsRoomCombatInt(monster.posX, desiredLogicalCenterX, step);
@@ -1498,9 +1493,9 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 				monster.facing = movedX < 0 ? 1 : 2;
 			else if (movedY != 0)
 				monster.facing = movedY < 0 ? 3 : 0;
-			(void)applyRoomActorPlacement(state, *entity, monster.posX, monster.posY, (float)monster.posZ);
+			(void)applyRoomActorPlacement(_scene.state, *entity, monster.posX, monster.posY, (float)monster.posZ);
 			if (monster.posZ != previousZ)
-				entityManager->reinsertSceneEntity(entity);
+				_entityManager->reinsertSceneEntity(entity);
 			(void)script.syncRuntimeMonsterRecord(monster);
 			debugC(3, kDebugCombat,
 				"Harvester: combat monster chase move monster='%s' live_center_dx=%d z_delta=%.2f engage=%d waypoint_tol=%d from=(%d,%d,z=%d) to=(%d,%d,z=%d) facing=%d",
@@ -1535,7 +1530,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 			continue;
 
 		AttackAnimationRange range;
-		if (!resolveMonsterAttackAnimationRange(engine, *entity, monsterCenterX, playerCenterX, range)) {
+		if (!resolveMonsterAttackAnimationRange(_engine, *entity, monsterCenterX, playerCenterX, range)) {
 			debugC(1, kDebugCombat,
 				"Harvester: combat monster attack skipped monster='%s' reason='no native attack bank' target_side=%s",
 				monster.monsterName.c_str(), playerCenterX < monsterCenterX ? "left" : "right");
@@ -1551,7 +1546,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 			monster.attackSound1.empty() && monster.attackSound2.empty() && monster.attackSound3.empty();
 		combatState.attackContactResolved = false;
 		combatState.attackTargetName = playerState.entity->getName();
-		combatState.attackCooldownSeedTick = now + engine.getRandomNumber(99);
+		combatState.attackCooldownSeedTick = now + _engine.getRandomNumber(99);
 		combatState.loggedPursuitStallDirections = 0;
 		monster.facing = range.resumeFacing;
 		entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
@@ -1573,19 +1568,15 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
 	return Common::kNoError;
 }
 
-Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
-		RoomSetupState &state, Player &player,
-		EntityManager *entityManager, SceneEntities &sceneRuntime,
-		CombatEffects &combatEffects,
-		Interaction &interactionProcessor, bool &needsRedraw) {
-	const RoomPlayerState &playerState = player.getState();
+Common::Error Combat::resolvePlayerAttackContact(bool &needsRedraw) {
+	const RoomPlayerState &playerState = _player.getState();
 	if (!playerState.attackActive || playerState.attackContactResolved || !playerState.entity ||
 			playerState.attackContactFrame < 0 ||
 			playerState.entity->getCurrentFrame() < playerState.attackContactFrame) {
 		return Common::kNoError;
 	}
 
-	player.markAttackContactResolved();
+	_player.markAttackContactResolved();
 	const int damageType = Player::resolveCombatLoadoutDamageType(playerState.combatLoadout);
 	const int damageAmount = Player::resolveCombatLoadoutDamageAmount(playerState.combatLoadout);
 	debugC(1, kDebugCombat,
@@ -1594,14 +1585,14 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 		Player::describeCombatLoadout(playerState.combatLoadout), damageAmount,
 		Player::describeCombatDamageType(damageType),
 		describeCombatTargetClass(playerState.attackTargetClassId), playerState.attackTargetName.c_str());
-	Script *script = engine.getScript();
+	Script *script = _engine.getScript();
 	if (!script)
 		return Common::kNoError;
 
-	captureOverlappingNpcAttackTarget(state, player, entityManager);
+	captureOverlappingNpcAttackTarget();
 	if (playerState.attackTargetClassId == kEntityClassNpc) {
 		Entity *npcEntity = nullptr;
-		NpcRecord *npc = selectNpcAttackContactTarget(state, playerState, entityManager, npcEntity);
+		NpcRecord *npc = selectNpcAttackContactTarget(npcEntity);
 		if (!npc) {
 			debugC(1, kDebugCombat,
 				"Harvester: combat player attack missed class='npc' target='%s' reason='no reachable npc'",
@@ -1614,7 +1605,7 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 		const bool jimmyFlagChanged = npc->npcName.equalsIgnoreCase("JIMMY")
 			? script->setRuntimeFlagValue("JIMMY_ATTACKED", true)
 			: false;
-		const int npcIndex = findRoomNpcIndexByName(state, npc->npcName);
+		const int npcIndex = findRoomNpcIndexByName(_scene.state, npc->npcName);
 		NpcCombatState *combatState =
 			npcIndex >= 0 && npcIndex < (int)_npcCombatStates.size()
 				? &_npcCombatStates[(uint)npcIndex]
@@ -1631,11 +1622,11 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 			return Common::kNoError;
 		if (runtimeChanged && npcEntity) {
 			const Common::String followTargetName = npc->monsterfyTargetName;
-			if (combatEffects.spawnCombatHitEffect(*npcEntity, followTargetName))
+			if (_combatEffects.spawnCombatHitEffect(*npcEntity, followTargetName))
 				needsRedraw = true;
 		}
 		if (runtimeChanged && combatState && npcEntity &&
-				beginNpcDeathTransition(engine, *npc, *npcEntity, *combatState, damageType, needsRedraw))
+				beginNpcDeathTransition(*npc, *npcEntity, *combatState, damageType, needsRedraw))
 			return Common::kNoError;
 		if (runtimeChanged) {
 			debugC(1, kDebugCombat,
@@ -1643,14 +1634,14 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 				npc->npcName.c_str(), damageType, npc->onDeathActionTag.c_str());
 			NpcCombatState fallbackState;
 			fallbackState.deathDamageType = damageType;
-			return finalizeNpcDeathTransition(engine, entityManager, sceneRuntime, *npc, combatState ? *combatState : fallbackState, interactionProcessor, needsRedraw);
+			return finalizeNpcDeathTransition(*npc, combatState ? *combatState : fallbackState, needsRedraw);
 		}
 
 		return Common::kNoError;
 	}
 
 	Entity *monsterEntity = nullptr;
-	MonsterRecord *monster = selectMonsterAttackContactTarget(state, player, entityManager, monsterEntity);
+	MonsterRecord *monster = selectMonsterAttackContactTarget(monsterEntity);
 	if (!monster || !monsterEntity) {
 		debugC(1, kDebugCombat,
 			"Harvester: combat player attack missed class='monster' target='%s' reason='no reachable monster'",
@@ -1662,7 +1653,7 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 	monster->currentHitPoints = MAX(0,
 		monster->currentHitPoints - damageAmount);
 	const int damageLanded = hitPointsBefore - monster->currentHitPoints;
-	const int monsterIndex = findRoomMonsterIndexByName(state, monster->monsterName);
+	const int monsterIndex = findRoomMonsterIndexByName(_scene.state, monster->monsterName);
 	MonsterCombatState *combatState =
 		monsterIndex >= 0 && monsterIndex < (int)_monsterCombatStates.size()
 			? &_monsterCombatStates[(uint)monsterIndex]
@@ -1673,8 +1664,8 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 		Player::describeCombatLoadout(playerState.combatLoadout), damageAmount,
 		Player::describeCombatDamageType(damageType), hitPointsBefore, monster->currentHitPoints);
 	if (damageLanded > 0) {
-		combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
-		startMonsterHitReaction(engine, combatEffects, *monster, *monsterEntity, combatState, playerState.attackFirstFrame, needsRedraw);
+		_combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
+		startMonsterHitReaction(*monster, *monsterEntity, combatState, playerState.attackFirstFrame, needsRedraw);
 	}
 	if (monster->currentHitPoints > 0) {
 		(void)script->syncRuntimeMonsterRecord(*monster);
@@ -1687,7 +1678,7 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 
 	DeathAnimationRange deathRange;
 	if (combatState && resolveMonsterDeathAnimationRange(*monsterEntity, monster->facing,
-			damageType, engine.isGoreEnabled(), deathRange)) {
+			damageType, _engine.isGoreEnabled(), deathRange)) {
 		combatState->deathActive = true;
 		combatState->deathFirstFrame = deathRange.firstFrame;
 		combatState->deathLastFrame = deathRange.lastFrame;
@@ -1698,19 +1689,19 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 		monsterEntity->setAnimationEnabled(true);
 		monsterEntity->setCurrentFrame(deathRange.firstFrame);
 		if (!monster->deathSound.empty())
-			(void)engine.playSound(monster->deathSound);
+			(void)_engine.playSound(monster->deathSound);
 		debugC(1, kDebugCombat,
 			"Harvester: combat monster death start target='%s' damage_type=%d frames=%d..%d gore=%d on_death='%s'",
 			monster->monsterName.c_str(), damageType,
 			deathRange.firstFrame, deathRange.lastFrame,
-			engine.isGoreEnabled(), monster->onDeathActionTag.c_str());
+			_engine.isGoreEnabled(), monster->onDeathActionTag.c_str());
 		needsRedraw = true;
 		return Common::kNoError;
 	}
 
 	monster->active = false;
 	monster->visible = false;
-	const bool herrillDeathQueued = queueInnerSanctumHerrillDeathAfterMuck(engine, state, *monster);
+	const bool herrillDeathQueued = queueInnerSanctumHerrillDeathAfterMuck(*monster);
 	debugC(1, kDebugCombat,
 		"Harvester: combat monster defeated target='%s' on_death='%s' fallback='no death bank'",
 		monster->monsterName.c_str(), monster->onDeathActionTag.c_str());
@@ -1731,21 +1722,21 @@ Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
 		interaction.visualRuntimeStateChanged = true;
 	}
 
-	return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+	return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
 }
 
-void Combat::initializeCombatStates(const RoomSetupState &state) {
-	_monsterCombatStates.resize(state.roomMonsters.size());
-	_npcCombatStates.resize(state.roomNpcs.size());
+void Combat::initializeCombatStates() {
+	_monsterCombatStates.resize(_scene.state.roomMonsters.size());
+	_npcCombatStates.resize(_scene.state.roomNpcs.size());
 	debugC(1, kDebugCombat, "Harvester: initialized room combat states monsters=%u npcs=%u",
 		_monsterCombatStates.size(), _npcCombatStates.size());
 }
 
-void Combat::resizeCombatStates(const RoomSetupState &state) {
-	if (_npcCombatStates.size() != state.roomNpcs.size())
-		_npcCombatStates.resize(state.roomNpcs.size());
-	if (_monsterCombatStates.size() != state.roomMonsters.size())
-		_monsterCombatStates.resize(state.roomMonsters.size());
+void Combat::resizeCombatStates() {
+	if (_npcCombatStates.size() != _scene.state.roomNpcs.size())
+		_npcCombatStates.resize(_scene.state.roomNpcs.size());
+	if (_monsterCombatStates.size() != _scene.state.roomMonsters.size())
+		_monsterCombatStates.resize(_scene.state.roomMonsters.size());
 }
 
 Common::Array<MonsterCombatState> Combat::prepareMonsterCombatStatesForSceneChange(
@@ -1773,10 +1764,10 @@ void Combat::commitMonsterCombatStatesForSceneChange(Common::Array<MonsterCombat
 	debugC(2, kDebugCombat, "Harvester: committed room monster combat states count=%u", _monsterCombatStates.size());
 }
 
-void Combat::resetForSceneRefresh(const RoomSetupState &state) {
+void Combat::resetForSceneRefresh() {
 	// NPC death state survives refresh and is resized by index at the next combat update.
 	_monsterCombatStates.clear();
-	_monsterCombatStates.resize(state.roomMonsters.size());
+	_monsterCombatStates.resize(_scene.state.roomMonsters.size());
 	debugC(2, kDebugCombat, "Harvester: reset room monster combat states count=%u retained_npcs=%u",
 		_monsterCombatStates.size(), _npcCombatStates.size());
 }
diff --git a/engines/harvester/combat.h b/engines/harvester/combat.h
index 4d8e9d2d8df..40b64d192a1 100644
--- a/engines/harvester/combat.h
+++ b/engines/harvester/combat.h
@@ -36,7 +36,6 @@ class InteractionCallbacks;
 class Interaction;
 class SceneEntities;
 class Player;
-struct RoomPlayerState;
 
 extern const int kRoomMonsterAnimationRate;
 extern const int kNativeMonsterAttackAnimationRate;
@@ -132,65 +131,61 @@ struct NpcCombatUpdateResult {
 
 class Combat {
 public:
-	void initializeCombatStates(const RoomSetupState &state);
+	Combat(HarvesterEngine &engine, SceneState &scene, Player &player,
+		EntityManager *entityManager, SceneEntities &sceneRuntime, CombatEffects &combatEffects,
+		const CheatSystem &cheats, InteractionCallbacks &callbacks, Interaction &interactionProcessor);
+	Combat(const Combat &) = delete;
+	Combat &operator=(const Combat &) = delete;
+
+	void initializeCombatStates();
 	// Keep prepared state separate until scene-entity mutations succeed.
 	Common::Array<MonsterCombatState> prepareMonsterCombatStatesForSceneChange(
 		const Common::Array<MonsterRecord> &previousMonsters, const Common::Array<MonsterRecord> &updatedMonsters) const;
 	void commitMonsterCombatStatesForSceneChange(Common::Array<MonsterCombatState> &&states);
-	void resetForSceneRefresh(const RoomSetupState &state);
+	void resetForSceneRefresh();
 
-	void capturePlayerAttackTargetForKeyboard(RoomSetupState &state, Player &player,
-		EntityManager *entityManager, bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const;
-	MonsterRecord *resolveKillTarget(RoomSetupState &state, const RoomPlayerState &playerState,
-		EntityManager *entityManager, const Common::Point &mousePos) const;
+	void capturePlayerAttackTargetForKeyboard(bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const;
+	MonsterRecord *resolveKillTarget(const Common::Point &mousePos) const;
 	// The caller clears the captured target before resolving the room hover state.
-	void capturePlayerAttackTarget(RoomSetupState &state, Player &player,
-		EntityManager *entityManager, const Common::Point &mousePos, const NpcRecord *hoverNpc) const;
-	void captureOverlappingNpcAttackTarget(RoomSetupState &state, Player &player, EntityManager *entityManager) const;
-	NpcRecord *selectNpcAttackContactTarget(RoomSetupState &state, const RoomPlayerState &playerState,
-		EntityManager *entityManager, Entity *&npcEntity) const;
-	MonsterRecord *selectMonsterAttackContactTarget(RoomSetupState &state, Player &player,
-		EntityManager *entityManager, Entity *&monsterEntity) const;
-
-	Common::Error killDebugActiveMonster(HarvesterEngine &engine, RoomSetupState &state,
-		Player &player, EntityManager *entityManager, const Common::Point &mousePos,
-		CombatEffects &combatEffects, Interaction &interactionProcessor, bool &needsRedraw);
-
-	Common::Error updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
-		Player &player, EntityManager *entityManager,
-		SceneEntities &sceneRuntime, CombatEffects &combatEffects, const CheatSystem &cheats,
-		InteractionCallbacks &callbacks, Interaction &interactionProcessor, bool &needsRedraw);
-	Common::Error resolvePlayerAttackContact(HarvesterEngine &engine,
-		RoomSetupState &state, Player &player,
-		EntityManager *entityManager, SceneEntities &sceneRuntime,
-		CombatEffects &combatEffects,
-		Interaction &interactionProcessor, bool &needsRedraw);
+	void capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *hoverNpc) const;
+	void captureOverlappingNpcAttackTarget() const;
+	NpcRecord *selectNpcAttackContactTarget(Entity *&npcEntity) const;
+	MonsterRecord *selectMonsterAttackContactTarget(Entity *&monsterEntity) const;
+
+	Common::Error killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw);
+
+	Common::Error updateRoomMonsterCombat(bool &needsRedraw);
+	Common::Error resolvePlayerAttackContact(bool &needsRedraw);
 
 private:
-	void resizeCombatStates(const RoomSetupState &state);
-	bool queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const;
+	void resizeCombatStates();
+	bool queueInnerSanctumHerrillDeathAfterMuck(const MonsterRecord &monster) const;
 	// Called after resizeCombatStates; NPC finalization ends the entire combat update.
-	NpcCombatUpdateResult updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
-		EntityManager *entityManager, SceneEntities &sceneRuntime,
-		Interaction &interactionProcessor, bool &needsRedraw);
-
-	bool beginNpcDeathTransition(HarvesterEngine &engine,
-		NpcRecord &npc, Entity &npcEntity, NpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const;
-	Common::Error finalizeNpcDeathTransition(HarvesterEngine &engine,
-		EntityManager *entityManager, SceneEntities &sceneRuntime, NpcRecord &npc, NpcCombatState &combatState,
-		Interaction &interactionProcessor, bool &needsRedraw) const;
-
-	void startMonsterHitReaction(HarvesterEngine &engine,
-		CombatEffects &combatEffects, MonsterRecord &monster, Entity &monsterEntity, MonsterCombatState *combatState,
-		int attackerAttackFirstFrame, bool &needsRedraw) const;
+	NpcCombatUpdateResult updateRoomNpcCombat(bool &needsRedraw);
+
+	bool beginNpcDeathTransition(NpcRecord &npc, Entity &npcEntity, NpcCombatState &combatState,
+		int deathDamageType, bool &needsRedraw) const;
+	Common::Error finalizeNpcDeathTransition(NpcRecord &npc, NpcCombatState &combatState, bool &needsRedraw) const;
 
-	MonsterRecord *findMonsterTargetAtPoint(RoomSetupState &state, EntityManager *entityManager, const Common::Point &point) const;
-	MonsterRecord *findOverlappingMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState, EntityManager *entityManager) const;
-	NpcRecord *findOverlappingNpcTarget(RoomSetupState &state, const RoomPlayerState &playerState, EntityManager *entityManager) const;
-	bool playerAttackCanReachTarget(RoomSetupState &state, const RoomPlayerState &playerState, Entity *targetEntity, int engageDistance = 0) const;
-	MonsterRecord *findProjectileContactMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
-		EntityManager *entityManager) const;
+	void startMonsterHitReaction(MonsterRecord &monster, Entity &monsterEntity, MonsterCombatState *combatState,
+		int attackerAttackFirstFrame, bool &needsRedraw) const;
 
+	MonsterRecord *findMonsterTargetAtPoint(const Common::Point &point) const;
+	MonsterRecord *findOverlappingMonsterTarget() const;
+	NpcRecord *findOverlappingNpcTarget() const;
+	bool playerAttackCanReachTarget(Entity *targetEntity, int engageDistance = 0) const;
+	MonsterRecord *findProjectileContactMonsterTarget() const;
+
+	HarvesterEngine &_engine;
+	// Refresh assigns new room data into this scene object.
+	SceneState &_scene;
+	Player &_player;
+	EntityManager *const _entityManager;
+	SceneEntities &_sceneRuntime;
+	CombatEffects &_combatEffects;
+	const CheatSystem &_cheats;
+	InteractionCallbacks &_callbacks;
+	Interaction &_interactionProcessor;
 	Common::Array<MonsterCombatState> _monsterCombatStates;
 	Common::Array<NpcCombatState> _npcCombatStates;
 };
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index 9e938eab48a..c917dc985b0 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -130,7 +130,9 @@ Scene::Scene(HarvesterEngine &engine, Flow &flow, SceneState &scene, EntityManag
 	  _presentation(engine, scene, view.art, view.inspectFont, view.useNativeInspectFont),
 	  _interactionProcessor(engine, flow, scene, _player, _pendingRegionName, _pendingTransition.roomName,
 		_pendingTransition.isRoomName, _pendingTransition.usesSavedRoomState, _pendingTransition.savedRoomState,
-		canExitCloseupToParent, *this) {
+		canExitCloseupToParent, *this),
+	  _combat(engine, scene, _player, entityManager, _sceneRuntime, _combatEffects,
+		cheats, *this, _interactionProcessor) {
 }
 
 void Scene::resetIdleState() {
@@ -247,7 +249,7 @@ bool Scene::refreshCurrentScene(bool preservePlayerPlacement) {
 	_sceneRuntime.restoreRoomMonsterAnimations();
 	_roomItems.hideCarriedRoomItem();
 	_player.rebindAfterSceneRefresh(_engine, _scene.state, _entityManager, preservePlayerPlacement);
-	_combat.resetForSceneRefresh(_scene.state);
+	_combat.resetForSceneRefresh();
 	_pendingRegionName.clear();
 	resetIdleState();
 	_flow.resetCursorAnimationSequence();
@@ -259,7 +261,7 @@ void Scene::initializePlayerState() {
 	_player.initialize(_scene.state,
 		_entityManager ? _entityManager->findSceneEntityByName("PLAYER") : nullptr,
 		_engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0);
-	_combat.initializeCombatStates(_scene.state);
+	_combat.initializeCombatStates();
 }
 
 void Scene::syncAnimatedRoomActorPlacement() {
@@ -582,7 +584,7 @@ void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsR
 			!idleState.active && !idleState.exiting) {
 		const uint32 now = Player::getRuntimeClockTicks();
 		if (_inputState.nextKeyboardAttackAllowedTick == 0 || (int32)(now - _inputState.nextKeyboardAttackAllowedTick) >= 0) {
-			_combat.capturePlayerAttackTargetForKeyboard(_scene.state, _player, _entityManager,
+			_combat.capturePlayerAttackTargetForKeyboard(
 				_inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown);
 			if (_player.startKeyboardAttackAnimation(_scene.state,
 					_inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown)) {
@@ -864,13 +866,11 @@ Common::Error Scene::tryActivatePassiveRegion(bool &needsRedraw) {
 }
 
 Common::Error Scene::resolvePlayerAttackContact(bool &needsRedraw) {
-	return _combat.resolvePlayerAttackContact(_engine, _scene.state, _player, _entityManager,
-		_sceneRuntime, _combatEffects, _interactionProcessor, needsRedraw);
+	return _combat.resolvePlayerAttackContact(needsRedraw);
 }
 
 Common::Error Scene::updateRoomMonsterCombat(bool &needsRedraw) {
-	return _combat.updateRoomMonsterCombat(_engine, _scene.state, _player, _entityManager,
-		_sceneRuntime, _combatEffects, _cheats, *this, _interactionProcessor, needsRedraw);
+	return _combat.updateRoomMonsterCombat(needsRedraw);
 }
 
 void Scene::syncCombatHitEffects(bool &needsRedraw) {
@@ -884,12 +884,11 @@ void Scene::updateIdleAnimation(bool &needsRedraw) {
 }
 
 void Scene::capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *npc) {
-	_combat.capturePlayerAttackTarget(_scene.state, _player, _entityManager, mousePos, npc);
+	_combat.capturePlayerAttackTarget(mousePos, npc);
 }
 
 Common::Error Scene::killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw) {
-	return _combat.killDebugActiveMonster(_engine, _scene.state, _player, _entityManager,
-		mousePos, _combatEffects, _interactionProcessor, needsRedraw);
+	return _combat.killDebugActiveMonster(mousePos, needsRedraw);
 }
 
 static const CftFontResource *findStartupFontByName(const HarvesterEngine &engine, const char *fontName) {
diff --git a/engines/harvester/scene.h b/engines/harvester/scene.h
index 733a276743e..f8a157b8d2f 100644
--- a/engines/harvester/scene.h
+++ b/engines/harvester/scene.h
@@ -126,7 +126,6 @@ private:
 	InventorySystem &_inventory;
 	SceneEntities _sceneRuntime;
 	Player _player;
-	Combat _combat;
 	CombatEffects _combatEffects;
 	InputState _inputState;
 	Common::String _pendingRegionName;
@@ -138,6 +137,7 @@ private:
 	ItemInteractions _roomItems;
 	RoomPresentation _presentation;
 	Interaction _interactionProcessor;
+	Combat _combat;
 };
 
 } // End of namespace Harvester


Commit: a99abfd26a1bc8b7a62a642a99cff4782cea7218
    https://github.com/scummvm/scummvm/commit/a99abfd26a1bc8b7a62a642a99cff4782cea7218
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: add an explicit execution context for scripts

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/cheats.cpp
    engines/harvester/cheats.h
    engines/harvester/combat.cpp
    engines/harvester/dialogue.cpp
    engines/harvester/dialogue.h
    engines/harvester/flow.cpp
    engines/harvester/flow.h
    engines/harvester/input.cpp
    engines/harvester/interaction.cpp
    engines/harvester/npc/dialogue_runtime.h
    engines/harvester/room_support.h
    engines/harvester/scene.cpp
    engines/harvester/script.cpp
    engines/harvester/script.h


diff --git a/engines/harvester/cheats.cpp b/engines/harvester/cheats.cpp
index e58bebe9158..0652269e829 100644
--- a/engines/harvester/cheats.cpp
+++ b/engines/harvester/cheats.cpp
@@ -180,7 +180,7 @@ CheatInputResult CheatSystem::matchKey(uint16 ascii, CheatId &cheatId) {
 }
 
 CheatInputResult CheatSystem::processKey(const Common::KeyState &key, Script &script,
-		const Common::String &roomName, InteractionResult &interaction,
+		const ScriptExecutionContext &executionContext, InteractionResult &interaction,
 		CheatActivation &activation) {
 	interaction = InteractionResult();
 	activation = CheatActivation();
@@ -188,12 +188,12 @@ CheatInputResult CheatSystem::processKey(const Common::KeyState &key, Script &sc
 	CheatId cheatId = kCheatMaxHealth;
 	const CheatInputResult inputResult = matchKey(key.ascii, cheatId);
 	if (inputResult == kCheatInputComplete)
-		execute(cheatId, script, roomName, interaction, activation);
+		execute(cheatId, script, executionContext, interaction, activation);
 
 	return inputResult;
 }
 
-void CheatSystem::execute(CheatId cheatId, Script &script, const Common::String &roomName,
+void CheatSystem::execute(CheatId cheatId, Script &script, const ScriptExecutionContext &executionContext,
 		InteractionResult &interaction, CheatActivation &activation) {
 	const bool hasHarvestBlade = script.isObjectInInventory(kHarvestBladeObjectName);
 	const bool requiresHarvestBlade = cheatId == kCheatLodgeLevel2 ||
@@ -243,17 +243,17 @@ void CheatSystem::execute(CheatId cheatId, Script &script, const Common::String
 		break;
 	case kCheatLodgeLevel2:
 		activation.hasInteraction = script.executeActionTag(
-			"FOUNTAIN_ART1", interaction, true, roomName);
+			"FOUNTAIN_ART1", interaction, true, executionContext);
 		break;
 	case kCheatLodgeLevel3:
 		activation.clearInteractionState = true;
 		activation.hasInteraction = script.executeActionTag(
-			"3RD_2_3RDNTRYR", interaction, true, roomName);
+			"3RD_2_3RDNTRYR", interaction, true, executionContext);
 		break;
 	case kCheatLodgeFinalLevel:
 		activation.clearInteractionState = true;
 		activation.hasInteraction = script.executeActionTag(
-			"L2_M1", interaction, true, roomName);
+			"L2_M1", interaction, true, executionContext);
 		break;
 	case kCheatInvincibility:
 		activation.clearInteractionState = true;
@@ -263,7 +263,7 @@ void CheatSystem::execute(CheatId cheatId, Script &script, const Common::String
 	case kCheatLodgeLevel1:
 		activation.clearInteractionState = true;
 		activation.hasInteraction = script.executeActionTag(
-			"WARP_TO_LODGE", interaction, true, roomName);
+			"WARP_TO_LODGE", interaction, true, executionContext);
 		break;
 	}
 
@@ -272,7 +272,7 @@ void CheatSystem::execute(CheatId cheatId, Script &script, const Common::String
 			!activation.hasInteraction) {
 		debugC(1, kDebugGeneral,
 			"Harvester: cheat action tag produced no interaction id='%s' room='%s'",
-			describe(cheatId), roomName.c_str());
+			describe(cheatId), executionContext.roomName().c_str());
 	}
 
 	debugC(2, kDebugGeneral,
diff --git a/engines/harvester/cheats.h b/engines/harvester/cheats.h
index fd6ab757a74..a22cd3027d8 100644
--- a/engines/harvester/cheats.h
+++ b/engines/harvester/cheats.h
@@ -32,6 +32,7 @@ namespace Harvester {
 
 class HarvesterEngine;
 class Script;
+class ScriptExecutionContext;
 struct InteractionResult;
 
 enum CheatInputResult {
@@ -53,7 +54,7 @@ public:
 	explicit CheatSystem(HarvesterEngine &engine);
 
 	CheatInputResult processKey(const Common::KeyState &key, Script &script,
-		const Common::String &roomName, InteractionResult &interaction,
+		const ScriptExecutionContext &executionContext, InteractionResult &interaction,
 		CheatActivation &activation);
 	void reset();
 	bool isPlayerDamageDisabled() const { return _playerDamageDisabled; }
@@ -72,7 +73,7 @@ private:
 
 	CheatInputResult matchKey(uint16 ascii, CheatId &cheatId);
 	static const char *describe(CheatId cheatId);
-	void execute(CheatId cheatId, Script &script, const Common::String &roomName,
+	void execute(CheatId cheatId, Script &script, const ScriptExecutionContext &executionContext,
 		InteractionResult &interaction, CheatActivation &activation);
 
 	HarvesterEngine &_engine;
diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat.cpp
index 8c59eb59662..64451dbcc14 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -1112,7 +1112,8 @@ Common::Error Combat::finalizeNpcDeathTransition(NpcRecord &npc, NpcCombatState
 	interaction.visualRuntimeStateChanged = runtimeChanged;
 	if (script && !npc.onDeathActionTag.empty()) {
 		InteractionResult deathInteraction;
-		if (script->executeActionTag(npc.onDeathActionTag, deathInteraction, true, npc.roomName)) {
+		if (script->executeActionTag(npc.onDeathActionTag, deathInteraction, true,
+				ScriptExecutionContext::forNpcDeath(npc))) {
 			interaction = Common::move(deathInteraction);
 			interaction.mutatedRuntimeState = true;
 			interaction.visualRuntimeStateChanged = true;
@@ -1242,7 +1243,7 @@ Common::Error Combat::killDebugActiveMonster(const Common::Point &mousePos, bool
 	if (!monster->onDeathActionTag.empty()) {
 		InteractionResult deathInteraction;
 		if (script->executeActionTag(
-				monster->onDeathActionTag, deathInteraction, true, monster->roomName)) {
+				monster->onDeathActionTag, deathInteraction, true, ScriptExecutionContext::forMonsterDeath(*monster))) {
 			interaction = Common::move(deathInteraction);
 			interaction.mutatedRuntimeState = true;
 			interaction.visualRuntimeStateChanged = true;
@@ -1305,7 +1306,7 @@ Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
 				if (!monster.onDeathActionTag.empty()) {
 					InteractionResult deathInteraction;
 					if (script.executeActionTag(
-							monster.onDeathActionTag, deathInteraction, true, monster.roomName)) {
+							monster.onDeathActionTag, deathInteraction, true, ScriptExecutionContext::forMonsterDeath(monster))) {
 						interaction = Common::move(deathInteraction);
 						interaction.mutatedRuntimeState = true;
 						interaction.visualRuntimeStateChanged = true;
@@ -1711,7 +1712,7 @@ Common::Error Combat::resolvePlayerAttackContact(bool &needsRedraw) {
 	if (!monster->onDeathActionTag.empty()) {
 		InteractionResult deathInteraction;
 		if (script->executeActionTag(
-				monster->onDeathActionTag, deathInteraction, true, monster->roomName)) {
+				monster->onDeathActionTag, deathInteraction, true, ScriptExecutionContext::forMonsterDeath(*monster))) {
 			interaction = Common::move(deathInteraction);
 			interaction.mutatedRuntimeState = true;
 			interaction.visualRuntimeStateChanged = true;
diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 2626ea93933..17fb1e6b568 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -1425,7 +1425,7 @@ void DialogueSystem::syncRuntimeSaveState(Common::Serializer &s) {
 }
 
 Common::Error DialogueSystem::runRoomNpcDialogue(const IndexedBitmap &backdrop, const byte *palette,
-		float paletteBrightness, const Common::String &currentRoomName,
+		float paletteBrightness, const ScriptExecutionContext &executionContext,
 		const NpcRecord &npc, const Common::String &usedItemName,
 		Flow &flow) {
 	Script *script = _engine.getScript();
@@ -1457,7 +1457,7 @@ Common::Error DialogueSystem::runRoomNpcDialogue(const IndexedBitmap &backdrop,
 		return Common::kReadingFailed;
 	// START_DIALOG may invoke an NPC assigned to another room.
 	DialogueRuntime runtime(
-		_engine, *script, *text, flow, currentRoomName, session.getGenericByeTopic(), session);
+		_engine, *script, *text, flow, executionContext, session.getGenericByeTopic(), session);
 
 	return handler->handleDialogue(runtime, usedItemName, _sharedDialogueState);
 }
diff --git a/engines/harvester/dialogue.h b/engines/harvester/dialogue.h
index d006caedf21..5ef2c8b8664 100644
--- a/engines/harvester/dialogue.h
+++ b/engines/harvester/dialogue.h
@@ -50,7 +50,7 @@ public:
 	void resetRoomNpcDialogueState();
 	void syncRuntimeSaveState(Common::Serializer &s);
 	Common::Error runRoomNpcDialogue(const IndexedBitmap &backdrop, const byte *palette,
-		float paletteBrightness, const Common::String &currentRoomName,
+		float paletteBrightness, const ScriptExecutionContext &executionContext,
 		const NpcRecord &npc, const Common::String &usedItemName,
 		Flow &flow);
 
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 83844608f46..6bb61ba9653 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -1751,12 +1751,12 @@ Common::Error Flow::runRoomMenu(const IndexedBitmap &backdrop, const byte *palet
 }
 
 Common::Error Flow::runRoomNpcDialogue(const IndexedBitmap &backdrop, const byte *palette,
-		float paletteBrightness, const Common::String &currentRoomName,
+		float paletteBrightness, const ScriptExecutionContext &executionContext,
 		const NpcRecord &npc, const Common::String &usedItemName) {
 	_queuedDialogueInteraction = InteractionResult();
 	_hasQueuedDialogueInteraction = false;
 	return _dialogue.runRoomNpcDialogue(backdrop, palette, paletteBrightness,
-		currentRoomName, npc, usedItemName, *this);
+		executionContext, npc, usedItemName, *this);
 }
 
 Common::Error Flow::runTownMapSelector(const Common::String &mapEntryName,
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index 212399c4849..804c01862ea 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -75,7 +75,7 @@ private:
 	Common::Error runRoomMenu(const IndexedBitmap &backdrop, const byte *palette,
 		float paletteBrightness, bool canSaveGame);
 	Common::Error runRoomNpcDialogue(const IndexedBitmap &backdrop, const byte *palette,
-		float paletteBrightness, const Common::String &currentRoomName,
+		float paletteBrightness, const ScriptExecutionContext &executionContext,
 		const NpcRecord &npc, const Common::String &usedItemName);
 	Common::Error runTownMapSelector(const Common::String &mapEntryName, Common::String &destinationEntranceName);
 	Common::Error runRoomLoop(const Common::String &entranceName);
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index d4db61fbeb5..abe6160033b 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -220,7 +220,7 @@ InputResult Input::handleRightButtonDown(bool &needsRedraw) {
 				InteractionResult interaction;
 				bool didTransition = false;
 				const bool executedActionTag = _engine.getScript()->executeActionTag(
-						secondaryAction.actionTag, interaction, true, _scene.state.roomName);
+						secondaryAction.actionTag, interaction, true, _scene.executionContext());
 				debugC(1, kDebugInventory,
 					"Harvester: inventory right click executeActionTag('%s') -> %d music='%s' cutscene='%s' nextRoom='%s'",
 					secondaryAction.actionTag.c_str(), executedActionTag,
@@ -574,7 +574,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 
 	InteractionResult interaction;
 	if (!_engine.getScript()->resolveObjectInteraction(
-			*clickedObject, interaction, _scene.state.roomName)) {
+			*clickedObject, interaction, _scene.executionContext())) {
 		if (canShowInspectText) {
 			beginInspectText(*clickedObject, resolvedInspectText);
 		} else if (hasInspectText) {
@@ -625,7 +625,7 @@ InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw)
 		InteractionResult cheatInteraction;
 		CheatActivation cheatActivation;
 		const CheatInputResult cheatInputResult = _cheats.processKey(
-			key, *script, _scene.state.roomName,
+			key, *script, _scene.executionContext(),
 			cheatInteraction, cheatActivation);
 		if (cheatInputResult != kCheatInputIgnored) {
 			if (cheatInputResult == kCheatInputComplete && cheatActivation.activated) {
diff --git a/engines/harvester/interaction.cpp b/engines/harvester/interaction.cpp
index 2cc52ad8511..09bd3be59c9 100644
--- a/engines/harvester/interaction.cpp
+++ b/engines/harvester/interaction.cpp
@@ -178,7 +178,7 @@ Common::Error ItemInteractions::moveRoomItemDirectlyToInventory(const ObjectReco
 	if (shouldDispatchPickupActionOnDirectInventoryTransfer(object)) {
 		InteractionResult pickupInteraction;
 		if (script->executeActionTag(
-				object.actionTag, pickupInteraction, true, _scene.state.roomName)) {
+				object.actionTag, pickupInteraction, true, _scene.executionContext())) {
 			Common::Error interactionError =
 				interactionProcessor.handleInteractionResult(
 					pickupInteraction, didTransition, Common::String());
@@ -229,7 +229,7 @@ Common::Error ItemInteractions::beginRoomItemCarry(const ObjectRecord &object,
 	if (shouldDispatchPickupActionOnCarryStart(object)) {
 		InteractionResult pickupInteraction;
 		if (script->executeActionTag(
-				object.actionTag, pickupInteraction, true, _scene.state.roomName)) {
+				object.actionTag, pickupInteraction, true, _scene.executionContext())) {
 			Common::Error interactionError =
 				interactionProcessor.handleInteractionResult(
 					pickupInteraction, didTransition, Common::String());
@@ -275,7 +275,7 @@ Common::Error ItemInteractions::handleSelectedItemTargetInteraction(const Object
 	const Common::String selectedItemName = getSelectedItemName();
 	InteractionResult interaction;
 	const bool handled = _engine.getScript()->resolveUseItemInteraction(
-		selectedItemName, target, interaction, _scene.state.roomName);
+		selectedItemName, target, interaction, _scene.executionContext());
 	if (!handled) {
 		debugC(3, kDebugRoom, "Harvester: item use miss selected='%s' target='%s'",
 			selectedItemName.c_str(), target.objectName.c_str());
@@ -310,7 +310,7 @@ Common::Error ItemInteractions::handleSelectedItemMiss(Interaction &interactionP
 	};
 	InteractionResult interaction;
 	_engine.getScript()->executeActionTag(responseTags[_engine.getRandomNumber(4)],
-		interaction, true, _scene.state.roomName);
+		interaction, true, _scene.executionContext());
 	bool didTransition = false;
 	Common::Error interactionError =
 		interactionProcessor.handleInteractionResult(interaction, didTransition, getSelectedItemName());
@@ -399,7 +399,7 @@ Common::Error Interaction::runRegionInteraction(const RegionRecord &region,
 		InventorySystem &inventory, bool &needsRedraw) {
 	const RoomPlayerState &playerState = _player.getState();
 	InteractionResult interaction;
-	if (!_engine.getScript()->resolveRegionInteraction(region, interaction, _scene.state.roomName))
+	if (!_engine.getScript()->resolveRegionInteraction(region, interaction, _scene.executionContext()))
 		return Common::kNoError;
 	debugC(2, kDebugRoom,
 		"Harvester: region interaction room='%s' region='%s' action='%s' player=(%d,%d,z=%.2f) next_room='%s'",
@@ -739,7 +739,7 @@ Common::Error Interaction::handleInteractionResult(const InteractionResult &inte
 		InteractionResult continuationInteraction;
 		if (_engine.getScript()->executeActionTag(
 				interaction.continuationTag, continuationInteraction, true,
-				_scene.state.roomName)) {
+				_scene.executionContext())) {
 			Common::Error interactionError =
 				handleInteractionResult(continuationInteraction, didTransition, usedItemName, interruptPlayerMovement);
 			if (interactionError.getCode() != Common::kNoError)
@@ -763,7 +763,7 @@ Common::Error Interaction::runScriptedDialogue(const Common::String &npcName,
 
 		Common::Error dialogueError = _flow.runRoomNpcDialogue(
 			dialogueBackdrop, _scene.palette, _scene.targetPaletteBrightness,
-			_scene.state.roomName, *dialogueNpc, usedItemName);
+			_scene.executionContext(), *dialogueNpc, usedItemName);
 		if (dialogueError.getCode() != Common::kNoError)
 			return dialogueError;
 	} else {
@@ -785,7 +785,7 @@ Common::Error Interaction::runScriptedDialogue(const Common::String &npcName,
 	if (!didTransition && !abortRemainingCommandChain && !continuationTag.empty()) {
 		InteractionResult continuationInteraction;
 		if (_engine.getScript()->executeActionTag(
-				continuationTag, continuationInteraction, true, _scene.state.roomName)) {
+				continuationTag, continuationInteraction, true, _scene.executionContext())) {
 			Common::Error interactionError =
 				handleInteractionResult(continuationInteraction, didTransition, usedItemName);
 			if (interactionError.getCode() != Common::kNoError)
diff --git a/engines/harvester/npc/dialogue_runtime.h b/engines/harvester/npc/dialogue_runtime.h
index 50b7ce3cfa2..3c5d901d116 100644
--- a/engines/harvester/npc/dialogue_runtime.h
+++ b/engines/harvester/npc/dialogue_runtime.h
@@ -59,11 +59,11 @@ public:
 	typedef DialogueKeywordMenuSelectionState KeywordMenuSelectionState;
 
 	DialogueRuntime(HarvesterEngine &engine, Script &script, Text &text,
-			Flow &flow, const Common::String &currentRoomName,
+			Flow &flow, const ScriptExecutionContext &executionContext,
 			const Common::String &genericByeTopic,
 			DialogueRuntimeCallbacks &callbacks)
 		: _engine(engine), _script(script), _text(text),
-		  _flow(flow), _currentRoomName(currentRoomName),
+		  _flow(flow), _executionContext(executionContext),
 		  _genericByeTopic(genericByeTopic),
 		  _callbacks(callbacks) {
 	}
@@ -72,11 +72,11 @@ public:
 	Script &startupScript() const { return _script; }
 	Text &startupText() const { return _text; }
 	Flow &startupFlow() const { return _flow; }
-	const Common::String &currentRoomName() const { return _currentRoomName; }
+	const Common::String &currentRoomName() const { return _executionContext.roomName(); }
 	const Common::String &genericByeTopic() const { return _genericByeTopic; }
 	bool executeActionTag(const Common::String &tag, InteractionResult &result,
 			bool allowTransitions = true) const {
-		return _script.executeActionTag(tag, result, allowTransitions, _currentRoomName);
+		return _script.executeActionTag(tag, result, allowTransitions, _executionContext);
 	}
 	void executeAndQueueAction(const Common::String &tag) const {
 		InteractionResult interaction;
@@ -140,7 +140,7 @@ private:
 	Script &_script;
 	Text &_text;
 	Flow &_flow;
-	const Common::String &_currentRoomName;
+	const ScriptExecutionContext _executionContext;
 	const Common::String &_genericByeTopic;
 	DialogueRuntimeCallbacks &_callbacks;
 	mutable KeywordMenuSelectionState _lastKeywordSelection;
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index 9ba5b049b58..fc8c0d122fb 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -44,6 +44,9 @@ struct IndexedBitmap;
 struct MenuTextConfig;
 
 struct SceneState {
+	ScriptExecutionContext executionContext() const & { return ScriptExecutionContext(state.roomName); }
+	ScriptExecutionContext executionContext() const && = delete;
+
 	RoomSetupState state;
 	byte palette[256 * 3] = { 0 };
 	Common::Array<ObjectRecord> sceneObjects;
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index c917dc985b0..4029346fe1b 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -319,7 +319,7 @@ Common::Error Scene::runRoomExitCommands() {
 		InteractionResult continuationInteraction;
 		if (!_engine.getScript()->executeActionTag(
 				exitInteraction.continuationTag, continuationInteraction, false,
-				_scene.state.roomName)) {
+				_scene.executionContext())) {
 			return Common::kNoError;
 		}
 
@@ -657,7 +657,7 @@ InputResult Scene::dispatchExpiredTimers(bool &needsRedraw) {
 			for (const Common::String &timerName : expiredTimerNames) {
 				InteractionResult timerInteraction;
 				if (!_engine.getScript()->executeTimerAction(
-						timerName, timerInteraction, true, _scene.state.roomName))
+						timerName, timerInteraction, true, _scene.executionContext()))
 					continue;
 
 				bool didTransition = false;
diff --git a/engines/harvester/script.cpp b/engines/harvester/script.cpp
index de71ae5f6e5..763586eb59a 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script.cpp
@@ -1735,7 +1735,8 @@ bool Script::executeRoomExitCommands(const Common::String &roomName,
 }
 
 bool Script::resolveObjectInteraction(const ObjectRecord &object, InteractionResult &result,
-		const Common::String &roomName) {
+		const ScriptExecutionContext &executionContext) {
+	const Common::String &roomName = executionContext.roomName();
 	result = InteractionResult();
 	Common::String commandRoomName = roomName;
 	if (commandRoomName.empty() &&
@@ -1777,7 +1778,8 @@ bool Script::resolveObjectInteraction(const ObjectRecord &object, InteractionRes
 }
 
 bool Script::resolveRegionInteraction(const RegionRecord &region, InteractionResult &result,
-		const Common::String &roomName) {
+		const ScriptExecutionContext &executionContext) {
+	const Common::String &roomName = executionContext.roomName();
 	result = InteractionResult();
 	if (region.actionTag.empty())
 		return false;
@@ -1800,7 +1802,8 @@ bool Script::resolveRegionInteraction(const RegionRecord &region, InteractionRes
 }
 
 bool Script::resolveUseItemInteraction(const Common::String &itemName, const ObjectRecord &target,
-		InteractionResult &result, const Common::String &roomName) {
+		InteractionResult &result, const ScriptExecutionContext &executionContext) {
+	const Common::String &roomName = executionContext.roomName();
 	result = InteractionResult();
 
 	const UseItemRecord *useItem = findUseItemRecord(itemName, target);
@@ -1840,13 +1843,14 @@ bool Script::executeDebugCommand(const CommandRecord &command,
 	CommandRecord debugCommand = command;
 	debugCommand.triggerTag = debugTag;
 	_commands.push_back(Common::move(debugCommand));
-	const bool handled = executeActionTag(debugTag, result, allowTransitions);
+	const bool handled = executeActionTag(debugTag, result, allowTransitions, ScriptExecutionContext());
 	_commands.pop_back();
 	return handled;
 }
 
 bool Script::executeActionTag(const Common::String &tag, InteractionResult &result,
-		bool allowTransitions, const Common::String &roomName) {
+		bool allowTransitions, const ScriptExecutionContext &executionContext) {
+	const Common::String &roomName = executionContext.roomName();
 	result = InteractionResult();
 	if (tag.empty())
 		return false;
@@ -1869,7 +1873,8 @@ bool Script::executeActionTag(const Common::String &tag, InteractionResult &resu
 }
 
 bool Script::executeTimerAction(const Common::String &timerName, InteractionResult &result,
-		bool allowTransitions, const Common::String &roomName) {
+		bool allowTransitions, const ScriptExecutionContext &executionContext) {
+	const Common::String &roomName = executionContext.roomName();
 	result = InteractionResult();
 	if (timerName.empty())
 		return false;
@@ -1904,7 +1909,7 @@ bool Script::executeTimerAction(const Common::String &timerName, InteractionResu
 
 bool Script::executeNestedActionTag(const Common::String &tag, InteractionResult &result,
 		bool allowTransitions) {
-	const bool handled = executeActionTag(tag, result, allowTransitions);
+	const bool handled = executeActionTag(tag, result, allowTransitions, ScriptExecutionContext());
 	if (handled)
 		result.abortRemainingCommandChain = true;
 
diff --git a/engines/harvester/script.h b/engines/harvester/script.h
index c3872803fae..0f6ade5c563 100644
--- a/engines/harvester/script.h
+++ b/engines/harvester/script.h
@@ -266,6 +266,30 @@ struct RoomSetupState {
 	Common::Array<AudioCommand> audioCommands;
 };
 
+class ScriptExecutionContext {
+public:
+	ScriptExecutionContext() = default;
+
+	static ScriptExecutionContext forNpcDeath(const NpcRecord &npc) {
+		return ScriptExecutionContext(npc.roomName);
+	}
+	static ScriptExecutionContext forMonsterDeath(const MonsterRecord &monster) {
+		return ScriptExecutionContext(monster.roomName);
+	}
+
+	const Common::String &roomName() const {
+		static const Common::String noRoom;
+		return _roomName ? *_roomName : noRoom;
+	}
+
+private:
+	friend struct SceneState;
+
+	explicit ScriptExecutionContext(const Common::String &roomName) : _roomName(&roomName) {}
+
+	const Common::String *const _roomName = nullptr;
+};
+
 enum StartupRoomTransitionKind {
 	kStartupRoomTransitionNone = 0,
 	kStartupRoomTransitionChangeRoom,
@@ -369,17 +393,17 @@ public:
 	bool executeRoomEnterCommands(const Common::String &roomName, InteractionResult &result);
 	bool executeRoomExitCommands(const Common::String &roomName, InteractionResult &result);
 	bool resolveObjectInteraction(const ObjectRecord &object, InteractionResult &result,
-		const Common::String &roomName = Common::String());
+		const ScriptExecutionContext &executionContext);
 	bool resolveRegionInteraction(const RegionRecord &region, InteractionResult &result,
-		const Common::String &roomName = Common::String());
+		const ScriptExecutionContext &executionContext);
 	bool resolveUseItemInteraction(const Common::String &itemName, const ObjectRecord &target,
-		InteractionResult &result, const Common::String &roomName = Common::String());
+		InteractionResult &result, const ScriptExecutionContext &executionContext);
 	bool executeDebugCommand(const CommandRecord &command, InteractionResult &result,
 		bool allowTransitions = true);
 	bool executeActionTag(const Common::String &tag, InteractionResult &result,
-		bool allowTransitions = true, const Common::String &roomName = Common::String());
+		bool allowTransitions, const ScriptExecutionContext &executionContext);
 	bool executeTimerAction(const Common::String &timerName, InteractionResult &result,
-		bool allowTransitions = true, const Common::String &roomName = Common::String());
+		bool allowTransitions, const ScriptExecutionContext &executionContext);
 	bool executeNestedActionTag(const Common::String &tag, InteractionResult &result,
 		bool allowTransitions = true);
 	bool isPickupObject(const ObjectRecord &object) const;


Commit: 4c2cd50a791c3814cbaf2e8832d2543944ef0a08
    https://github.com/scummvm/scummvm/commit/4c2cd50a791c3814cbaf2e8832d2543944ef0a08
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Move font lookup into Text

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/dialogue.cpp
    engines/harvester/flow.cpp
    engines/harvester/menu.cpp
    engines/harvester/scene.cpp
    engines/harvester/text.cpp
    engines/harvester/text.h


diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 17fb1e6b568..48b0992f189 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -207,19 +207,6 @@ static void syncDialogueSharedState(Common::Serializer &s, DialogueSharedState &
 	syncDialogueInt(s, state.dialogueStateD2f30);
 }
 
-static const CftFontResource *findStartupFontByName(const HarvesterEngine &engine, const char *fontName) {
-	const Text *text = engine.getText();
-	if (!text || !fontName)
-		return nullptr;
-
-	for (const CftFontResource &font : text->getFonts()) {
-		if (font.name.equalsIgnoreCase(fontName))
-			return &font;
-	}
-
-	return nullptr;
-}
-
 static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
 	if (!bitmap.isValid())
 		return;
@@ -435,12 +422,12 @@ public:
 			return;
 		}
 
-		if (const CftFontResource *subtitleFontResource = findStartupFontByName(_engine, "TEXTFONT")) {
+		if (const CftFontResource *subtitleFontResource = _text->findFont("TEXTFONT")) {
 			_subtitleCftFont.reset(new HarvesterCftFont(*subtitleFontResource));
 			if (!_subtitleCftFont->isValid())
 				_subtitleCftFont.reset();
 		}
-		if (const CftFontResource *menuFontResource = findStartupFontByName(_engine, "TEXTFNT2")) {
+		if (const CftFontResource *menuFontResource = _text->findFont("TEXTFNT2")) {
 			_menuCftFont.reset(new HarvesterCftFont(*menuFontResource));
 			if (!_menuCftFont->isValid())
 				_menuCftFont.reset();
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 6bb61ba9653..359d826ffc0 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -120,19 +120,6 @@ static const int kCursorSequenceTransition = 6;
 static const int kCursorSequenceNeutral = 7;
 } // End of anonymous namespace
 
-static const CftFontResource *findStartupFontByName(const HarvesterEngine &engine, const char *fontName) {
-	const Text *text = engine.getText();
-	if (!text || !fontName)
-		return nullptr;
-
-	for (const CftFontResource &font : text->getFonts()) {
-		if (font.name.equalsIgnoreCase(fontName))
-			return &font;
-	}
-
-	return nullptr;
-}
-
 static byte findNearestPaletteColor(const byte *palette, byte red, byte green, byte blue) {
 	if (!palette)
 		return 0;
@@ -1639,7 +1626,8 @@ Common::Error Flow::runDemoIntroduction() {
 	ResourceManager *resources = _engine.getResources();
 	EntityManager *entityManager = _engine.getRuntimeEntities();
 	const Art *art = _engine.getArt();
-	const CftFontResource *fontResource = findStartupFontByName(_engine, "TEXTFONT");
+	const Text *text = _engine.getText();
+	const CftFontResource *fontResource = text ? text->findFont("TEXTFONT") : nullptr;
 	if (!screen || !resources || !art || !fontResource)
 		return Common::kReadingFailed;
 
@@ -1768,7 +1756,8 @@ Common::Error Flow::runTownMapSelector(const Common::String &mapEntryName,
 	Graphics::Screen *screen = _engine.getScreen();
 	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
 	Common::ScopedPtr<HarvesterCftFont> townMapFont;
-	if (const CftFontResource *townMapFontResource = findStartupFontByName(_engine, "TEXTFONT")) {
+	const Text *text = _engine.getText();
+	if (const CftFontResource *townMapFontResource = text ? text->findFont("TEXTFONT") : nullptr) {
 		townMapFont.reset(new HarvesterCftFont(*townMapFontResource));
 		if (townMapFont->isValid())
 			font = townMapFont.get();
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 0e83333470b..7c6c1f8e91c 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -182,19 +182,6 @@ private:
 static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y);
 static int getNativeRoomMenuLineHeight(const Graphics::Font &selectedFont);
 
-static const CftFontResource *findStartupFontByName(const HarvesterEngine &engine, const char *fontName) {
-	const Text *text = engine.getText();
-	if (!text || !fontName)
-		return nullptr;
-
-	for (const CftFontResource &font : text->getFonts()) {
-		if (font.name.equalsIgnoreCase(fontName))
-			return &font;
-	}
-
-	return nullptr;
-}
-
 static int clampStartupOptionLevel(int level) {
 	if (level < 0)
 		return 0;
@@ -1494,8 +1481,9 @@ Common::Error MenuSystem::runSaveSlotMenu(Flow &flow, SaveLoadMode mode, bool &c
 	completed = false;
 	ResourceManager *resources = _engine.getResources();
 	MetaEngine *metaEngine = _engine.getMetaEngine();
-	const CftFontResource *slotNameFontResource = findStartupFontByName(_engine, "MEDFONT1");
-	const CftFontResource *slotLabelFontResource = findStartupFontByName(_engine, "MEDFONT2");
+	const Text *text = _engine.getText();
+	const CftFontResource *slotNameFontResource = text ? text->findFont("MEDFONT1") : nullptr;
+	const CftFontResource *slotLabelFontResource = text ? text->findFont("MEDFONT2") : nullptr;
 	if (!resources || !metaEngine || !slotNameFontResource || !slotLabelFontResource)
 		return Common::kReadingFailed;
 	if (mode == kSaveMode ? !_engine.canSaveGameStateCurrently() : !_engine.canLoadGameStateCurrently())
@@ -1662,8 +1650,9 @@ Common::Error MenuSystem::runConfirmPrompt(const IndexedBitmap &backdrop, const
 		float paletteBrightness, Flow &flow, const Common::String &promptText,
 		bool &confirmed) {
 	const Art *art = _engine.getArt();
-	const CftFontResource *promptFontResource = findStartupFontByName(_engine, "MEDFONT1");
-	const CftFontResource *choiceFontResource = findStartupFontByName(_engine, "MEDFONT2");
+	const Text *text = _engine.getText();
+	const CftFontResource *promptFontResource = text ? text->findFont("MEDFONT1") : nullptr;
+	const CftFontResource *choiceFontResource = text ? text->findFont("MEDFONT2") : nullptr;
 	if (!art || !promptFontResource || !choiceFontResource)
 		return Common::kReadingFailed;
 
@@ -1762,8 +1751,9 @@ Common::Error MenuSystem::runPasswordPrompt(const IndexedBitmap &backdrop, const
 		float paletteBrightness, Flow &flow, bool drawLogo, Common::String &password,
 		bool &accepted) const {
 	const Art *art = _engine.getArt();
-	const CftFontResource *titleFontResource = findStartupFontByName(_engine, "HARVFONT");
-	const CftFontResource *entryFontResource = findStartupFontByName(_engine, "HARVFNT2");
+	const Text *text = _engine.getText();
+	const CftFontResource *titleFontResource = text ? text->findFont("HARVFONT") : nullptr;
+	const CftFontResource *entryFontResource = text ? text->findFont("HARVFNT2") : nullptr;
 	if (!art || !titleFontResource || !entryFontResource)
 		return Common::kReadingFailed;
 
@@ -1881,8 +1871,9 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 	const Art *art = _engine.getArt();
 	Script *script = _engine.getScript();
 	ResourceManager *resources = _engine.getResources();
-	const CftFontResource *selectedFontResource = findStartupFontByName(_engine, "HARVFONT");
-	const CftFontResource *unselectedFontResource = findStartupFontByName(_engine, "HARVFNT2");
+	const Text *text = _engine.getText();
+	const CftFontResource *selectedFontResource = text ? text->findFont("HARVFONT") : nullptr;
+	const CftFontResource *unselectedFontResource = text ? text->findFont("HARVFNT2") : nullptr;
 	if (!art || !script || !resources || !selectedFontResource || !unselectedFontResource)
 		return Common::kReadingFailed;
 
@@ -2333,8 +2324,9 @@ void MenuSystem::renderBackdropMenuScreen(const IndexedBitmap &backdrop, const b
 		int selectedItem) const {
 	Graphics::Screen *screen = _engine.getScreen();
 	const Art *art = _engine.getArt();
-	const CftFontResource *selectedFontResource = findStartupFontByName(_engine, "HARVFONT");
-	const CftFontResource *unselectedFontResource = findStartupFontByName(_engine, "HARVFNT2");
+	const Text *text = _engine.getText();
+	const CftFontResource *selectedFontResource = text ? text->findFont("HARVFONT") : nullptr;
+	const CftFontResource *unselectedFontResource = text ? text->findFont("HARVFNT2") : nullptr;
 	if (!screen || !art || !selectedFontResource || !unselectedFontResource || !backdrop.isValid())
 		return;
 
@@ -2384,7 +2376,8 @@ int MenuSystem::getMenuItemAt(const Common::Point &mousePos,
 
 int MenuSystem::getBackdropMenuItemAt(const Common::Point &mousePos,
 		const Common::Array<Common::String> &menuItems) const {
-	const CftFontResource *selectedFontResource = findStartupFontByName(_engine, "HARVFONT");
+	const Text *text = _engine.getText();
+	const CftFontResource *selectedFontResource = text ? text->findFont("HARVFONT") : nullptr;
 	if (!selectedFontResource)
 		return getMenuItemAt(mousePos, menuItems);
 
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index 4029346fe1b..d90da2d6beb 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -891,19 +891,6 @@ Common::Error Scene::killDebugActiveMonster(const Common::Point &mousePos, bool
 	return _combat.killDebugActiveMonster(mousePos, needsRedraw);
 }
 
-static const CftFontResource *findStartupFontByName(const HarvesterEngine &engine, const char *fontName) {
-	const Text *text = engine.getText();
-	if (!text || !fontName)
-		return nullptr;
-
-	for (const CftFontResource &font : text->getFonts()) {
-		if (font.name.equalsIgnoreCase(fontName))
-			return &font;
-	}
-
-	return nullptr;
-}
-
 SceneRunner::SceneRunner(HarvesterEngine &engine,
 		InventorySystem &inventory, CheatSystem &cheats)
 	: _engine(engine), _inventory(inventory), _cheats(cheats) {
@@ -995,8 +982,9 @@ Common::Error SceneRunner::runRoomLoop(Flow &flow, const Common::String &targetN
 		Graphics::Screen *screen = _engine.getScreen();
 		const Art *art = _engine.getArt();
 		const Graphics::Font *bodyFont = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
-		const CftFontResource *promptFontResource = findStartupFontByName(_engine, "MEDFONT1");
-		const CftFontResource *inventoryTooltipFontResource = findStartupFontByName(_engine, "TEXTFONT");
+		const Text *text = _engine.getText();
+		const CftFontResource *promptFontResource = text ? text->findFont("MEDFONT1") : nullptr;
+		const CftFontResource *inventoryTooltipFontResource = text ? text->findFont("TEXTFONT") : nullptr;
 		Common::ScopedPtr<HarvesterCftFont> promptCftFont;
 		Common::ScopedPtr<HarvesterCftFont> inventoryTooltipCftFont;
 		const Graphics::Font *promptFont = bodyFont;
diff --git a/engines/harvester/text.cpp b/engines/harvester/text.cpp
index dffc9693187..f295f06117e 100644
--- a/engines/harvester/text.cpp
+++ b/engines/harvester/text.cpp
@@ -277,6 +277,18 @@ void Text::decodeXorText(Common::Array<byte> &data) const {
 	}
 }
 
+const CftFontResource *Text::findFont(const char *fontName) const {
+	if (!fontName)
+		return nullptr;
+
+	for (const CftFontResource &font : _fonts) {
+		if (font.name.equalsIgnoreCase(fontName))
+			return &font;
+	}
+
+	return nullptr;
+}
+
 bool Text::resolveDialogueSubtitle(int wavId, Common::String &text) const {
 	text.clear();
 	if (wavId <= 0 || _dialogueEntries.empty() || _dialogueData.empty())
diff --git a/engines/harvester/text.h b/engines/harvester/text.h
index 3d64d0879ba..e72c2c75778 100644
--- a/engines/harvester/text.h
+++ b/engines/harvester/text.h
@@ -59,7 +59,7 @@ public:
 	const Common::Array<byte> &getDialogueData() const { return _dialogueData; }
 	const Common::Array<DialogueIndexEntry> &getDialogueEntries() const { return _dialogueEntries; }
 	const Common::Array<Common::String> &getDialogueResponseLines() const { return _dialogueResponseLines; }
-	const Common::Array<CftFontResource> &getFonts() const { return _fonts; }
+	const CftFontResource *findFont(const char *fontName) const;
 	bool resolveDialogueSubtitle(int wavId, Common::String &text) const;
 	Common::String getDialogueResponseLine(int zeroBasedIndex) const;
 


Commit: 736efc4b9db42c063e2ee433596caaa46fc381c8
    https://github.com/scummvm/scummvm/commit/736efc4b9db42c063e2ee433596caaa46fc381c8
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Move scene drawing into RoomPresentation

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/flow.cpp
    engines/harvester/input.cpp
    engines/harvester/room_presentation.cpp
    engines/harvester/room_presentation.h
    engines/harvester/room_support.h
    engines/harvester/scene.cpp


diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 359d826ffc0..b9de7ecbd8d 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -43,6 +43,7 @@
 #include "harvester/resources.h"
 #include "harvester/entity.h"
 #include "harvester/scene_entities.h"
+#include "harvester/room_presentation.h"
 #include "harvester/art.h"
 #include "harvester/monster.h"
 #include "harvester/player.h"
@@ -104,12 +105,6 @@ static const int kTownMapCursorHitExtent = 5;
 static const byte kIdentTextColor = 0xd3;
 static const byte kShadowColor = 0;
 static const byte kTownMapLabelColor = 0x28;
-static const int kRoomDebugLabelPaddingX = 3;
-static const int kRoomDebugLabelPaddingY = 2;
-static const int kRoomDebugRoomNameX = 4;
-static const int kRoomDebugRoomNameY = 4;
-static const int kTimerDebugOverlayXDivisor = 8;
-static const int kTimerDebugOverlayLineSpacing = 1;
 
 static const int kCursorSequenceWalk = 0;
 static const int kCursorSequenceExamine = 1;
@@ -120,294 +115,6 @@ static const int kCursorSequenceTransition = 6;
 static const int kCursorSequenceNeutral = 7;
 } // End of anonymous namespace
 
-static byte findNearestPaletteColor(const byte *palette, byte red, byte green, byte blue) {
-	if (!palette)
-		return 0;
-
-	byte bestIndex = 0;
-	uint32 bestDistance = 0xffffffffu;
-	for (int i = 0; i < 256; ++i) {
-		const int paletteOffset = i * 3;
-		const int dr = (int)palette[paletteOffset] - red;
-		const int dg = (int)palette[paletteOffset + 1] - green;
-		const int db = (int)palette[paletteOffset + 2] - blue;
-		const uint32 distance = (uint32)(dr * dr + dg * dg + db * db);
-		if (distance < bestDistance) {
-			bestDistance = distance;
-			bestIndex = (byte)i;
-		}
-	}
-
-	return bestIndex;
-}
-
-static Common::String resolveRoomDebugObjectLabel(HarvesterEngine &engine, const ObjectRecord &object) {
-	if (Script *script = engine.getScript()) {
-		const Common::String resolvedLabel = script->resolveObjectLabel(object);
-		if (!resolvedLabel.empty())
-			return resolvedLabel;
-	}
-
-	return object.objectName;
-}
-
-static Common::String resolveRoomDebugNpcLabel(const NpcRecord &npc) {
-	Common::String label = !npc.entityInitArg.empty() ? npc.entityInitArg : npc.npcName;
-	for (uint i = 0; i < label.size(); ++i) {
-		if (label[i] == '_')
-			label.setChar(' ', i);
-	}
-
-	return label;
-}
-
-static Common::String resolveCombatDebugLabel(const Common::String &name, int currentHitPoints, int maxHitPoints) {
-	Common::String label = name;
-	for (uint i = 0; i < label.size(); ++i) {
-		if (label[i] == '_')
-			label.setChar(' ', i);
-	}
-
-	return Common::String::format("%s (%d/%d)", label.c_str(), currentHitPoints, maxHitPoints);
-}
-
-static void drawRoomDebugLabel(Graphics::Screen &screen, const Graphics::Font &font,
-		const Common::String &text, int x, int y, byte textColor, byte backgroundColor) {
-	if (text.empty())
-		return;
-
-	const int labelWidth = font.getStringWidth(text) + kRoomDebugLabelPaddingX * 2;
-	const int labelHeight = font.getFontHeight() + kRoomDebugLabelPaddingY * 2;
-	const int drawX = CLIP<int>(x, 0, MAX(0, screen.w - labelWidth));
-	const int drawY = CLIP<int>(y, 0, MAX(0, screen.h - labelHeight));
-	const Common::Rect background(drawX, drawY,
-		MIN<int>(screen.w, drawX + labelWidth), MIN<int>(screen.h, drawY + labelHeight));
-	screen.fillRect(background, backgroundColor);
-	font.drawString(&screen, text,
-		background.left + kRoomDebugLabelPaddingX,
-		background.top + kRoomDebugLabelPaddingY,
-		MAX(0, background.width() - kRoomDebugLabelPaddingX * 2),
-		textColor);
-}
-
-static void drawRoomDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
-		const SceneState &scene) {
-	if (!engine.isRoomDebugEnabled())
-		return;
-
-	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
-	if (!font)
-		return;
-
-	byte displayPalette[256 * 3];
-	screen.getPalette(displayPalette);
-	const byte black = findNearestPaletteColor(displayPalette, 0x00, 0x00, 0x00);
-	const byte white = findNearestPaletteColor(displayPalette, 0xff, 0xff, 0xff);
-	const byte darkGray = findNearestPaletteColor(displayPalette, 0x40, 0x40, 0x40);
-	const byte red = findNearestPaletteColor(displayPalette, 0xff, 0x00, 0x00);
-	Common::String roomName = scene.state.roomName;
-	roomName.toUppercase();
-
-	drawRoomDebugLabel(screen, *font, roomName,
-		kRoomDebugRoomNameX, kRoomDebugRoomNameY, black, white);
-
-	for (const RegionRecord &region : scene.sceneRegions)
-		drawRoomDebugLabel(screen, *font, region.regionName, region.left, region.top, white, darkGray);
-
-	EntityManager *entityManager = engine.getRuntimeEntities();
-	for (const ObjectRecord &object : scene.sceneObjects) {
-		const Entity *entity = entityManager
-			? entityManager->findSceneEntityByName(object.objectName)
-			: nullptr;
-		if (entity && entity->getClassId() == kEntityClassBackground)
-			continue;
-
-		drawRoomDebugLabel(screen, *font, resolveRoomDebugObjectLabel(engine, object),
-			object.currentX, object.currentY, white, black);
-	}
-
-	for (const NpcRecord &npc : scene.state.roomNpcs) {
-		const Entity *entity = entityManager
-			? entityManager->findSceneEntityByName(npc.npcName)
-			: nullptr;
-		if (!entity || entity->getClassId() != kEntityClassNpc || !entity->isVisible())
-			continue;
-
-		const Common::Rect npcRect = entity->getScreenRect();
-		drawRoomDebugLabel(screen, *font, resolveRoomDebugNpcLabel(npc),
-			npcRect.left, npcRect.top, white, red);
-	}
-}
-
-static void drawCombatDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
-		const SceneState &scene) {
-	if (!engine.isCombatDebugEnabled())
-		return;
-
-	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
-	EntityManager *entityManager = engine.getRuntimeEntities();
-	Script *script = engine.getScript();
-	if (!font || !entityManager)
-		return;
-
-	byte displayPalette[256 * 3];
-	screen.getPalette(displayPalette);
-	const byte black = findNearestPaletteColor(displayPalette, 0x00, 0x00, 0x00);
-	const byte white = findNearestPaletteColor(displayPalette, 0xff, 0xff, 0xff);
-	const byte red = findNearestPaletteColor(displayPalette, 0xff, 0x00, 0x00);
-
-	const Entity *playerEntity = entityManager->findSceneEntityByName(kPlayerActorEntityName);
-	if (script && playerEntity &&
-			playerEntity->getClassId() == kEntityClassPlayer &&
-			playerEntity->isVisible()) {
-		const Common::Rect playerRect = playerEntity->getScreenRect();
-		drawRoomDebugLabel(screen, *font,
-			resolveCombatDebugLabel(playerEntity->getName(),
-				script->getPlayerCurrentHitPoints(), Script::kDefaultPlayerHitPoints),
-			playerRect.left, playerRect.top, white, red);
-	}
-
-	for (const MonsterRecord &monster : scene.state.roomMonsters) {
-		const Entity *entity = entityManager->findSceneEntityByName(monster.monsterName);
-		if (!entity || entity->getClassId() != kEntityClassMonster || !entity->isVisible())
-			continue;
-
-		const Common::Rect monsterRect = entity->getScreenRect();
-		drawRoomDebugLabel(screen, *font,
-			resolveCombatDebugLabel(monster.monsterName,
-				monster.currentHitPoints, monster.initialHitPoints),
-			monsterRect.left, monsterRect.top, white, black);
-	}
-}
-
-static Common::Rect getPathfindingMovementBounds(const Graphics::Screen &screen,
-		const RoomSetupState &state) {
-	if (!Player::supportsMovementBand(state))
-		return Common::Rect();
-
-	return Common::Rect(0, state.roomMaxZScreenY, screen.w, state.roomMinZScreenY + 1);
-}
-
-static void drawClippedDebugFrame(Graphics::Screen &screen, Common::Rect rect, byte color) {
-	rect.clip(screen.getBounds());
-	if (rect.isEmpty())
-		return;
-
-	screen.frameRect(rect, color);
-}
-
-static bool canEntityAffectPlayerPathfinding(const Entity &entity) {
-	if (!entity.isVisible())
-		return false;
-
-	switch (entity.getClassId()) {
-	case kEntityClassCursor:
-	case kEntityClassBackground:
-	case kEntityClassPlayer:
-	case kEntityClassRectHotspot:
-	case kEntityClassRectHotspot19:
-	case kEntityClassTimer:
-		return false;
-	default:
-		break;
-	}
-
-	return entity.getClassId() == kEntityClassDisabledHotspot || entity.hasFrames();
-}
-
-static void drawPathfindingBlockerOutline(Graphics::Screen &screen, EntityManager &entityManager,
-		const Common::String &entityName, const Common::Rect &movementBounds, byte color) {
-	const Entity *entity = entityManager.findSceneEntityByName(entityName);
-	if (!entity || !canEntityAffectPlayerPathfinding(*entity))
-		return;
-
-	Common::Rect rect = entity->getScreenRect();
-	if (rect.isEmpty())
-		return;
-
-	Common::Rect overlap = rect;
-	overlap.clip(movementBounds);
-	if (overlap.isEmpty())
-		return;
-
-	drawClippedDebugFrame(screen, rect, color);
-}
-
-static void drawPathfindingDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
-		const SceneState &scene) {
-	if (!engine.isPathfindingDebugEnabled())
-		return;
-
-	byte displayPalette[256 * 3];
-	screen.getPalette(displayPalette);
-	const byte blue = findNearestPaletteColor(displayPalette, 0x00, 0x40, 0xff);
-	const byte green = findNearestPaletteColor(displayPalette, 0x00, 0xff, 0x00);
-	const Common::Rect movementBounds = getPathfindingMovementBounds(screen, scene.state);
-	if (movementBounds.isEmpty())
-		return;
-
-	drawClippedDebugFrame(screen, movementBounds, blue);
-
-	EntityManager *entityManager = engine.getRuntimeEntities();
-	if (!entityManager)
-		return;
-
-	for (const ObjectRecord &object : scene.sceneObjects)
-		drawPathfindingBlockerOutline(screen, *entityManager,
-			object.objectName, movementBounds, green);
-
-	for (const AnimRecord &anim : scene.sceneAnimations)
-		drawPathfindingBlockerOutline(screen, *entityManager,
-			anim.animName, movementBounds, green);
-}
-
-static Common::String resolveTimerDebugLabel(const TimerRecord &timer, const Entity &entity) {
-	const Common::String actionTag = timer.arg2.empty() ? Common::String("<none>") : timer.arg2;
-
-	return Common::String::format("%s start=%d current=%d action=%s",
-		timer.timerName.c_str(), entity.getTimerInitialValue(),
-		entity.getTimerCurrentValue(), actionTag.c_str());
-}
-
-static void drawTimerDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
-		const SceneState &) {
-	if (!engine.isTimerDebugEnabled())
-		return;
-
-	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
-	EntityManager *entityManager = engine.getRuntimeEntities();
-	Script *script = engine.getScript();
-	if (!font || !entityManager || !script)
-		return;
-
-	Common::Array<Common::String> labels;
-	for (const TimerRecord &timer : script->getTimers()) {
-		const Entity *entity = entityManager->findSceneEntityByName(timer.timerName);
-		if (!entity || entity->getClassId() != kEntityClassTimer || !entity->isTimerEnabled())
-			continue;
-
-		const TimerRecord *runtimeTimer = script->findRuntimeTimerRecord(timer.timerName);
-		labels.push_back(resolveTimerDebugLabel(runtimeTimer ? *runtimeTimer : timer, *entity));
-	}
-	if (labels.empty())
-		return;
-
-	byte displayPalette[256 * 3];
-	screen.getPalette(displayPalette);
-	const byte black = findNearestPaletteColor(displayPalette, 0x00, 0x00, 0x00);
-	const byte white = findNearestPaletteColor(displayPalette, 0xff, 0xff, 0xff);
-
-	const int labelHeight = font->getFontHeight() + kRoomDebugLabelPaddingY * 2;
-	const int lineHeight = labelHeight + kTimerDebugOverlayLineSpacing;
-	const int totalHeight = (int)labels.size() * labelHeight +
-		(int)(labels.size() - 1) * kTimerDebugOverlayLineSpacing;
-	const int x = screen.w / kTimerDebugOverlayXDivisor;
-	const int y = CLIP<int>((screen.h - totalHeight) / 2, 0, MAX(0, screen.h - totalHeight));
-
-	for (uint i = 0; i < labels.size(); ++i)
-		drawRoomDebugLabel(screen, *font, labels[i], x, y + (int)i * lineHeight, white, black);
-}
-
 static int clampTownMapPanelIndex(int panelIndex) {
 	return CLIP<int>(panelIndex, 0, ARRAYSIZE(kTownMapBitmapPaths) - 1);
 }
@@ -760,18 +467,6 @@ bool shouldRunStartupRoomProbe() {
 		ConfMan.getBool("harvester_debug_probe_startup_room");
 }
 
-void drawRoomScene(HarvesterEngine &engine, Graphics::Screen &screen, const SceneState &scene,
-		float brightness) {
-	setScaledPalette(screen, scene.palette, brightness);
-	screen.fillRect(screen.getBounds(), 0);
-	if (engine.getRuntimeEntities())
-		engine.getRuntimeEntities()->drawSceneEntities(screen);
-	drawRoomDebugOverlay(engine, screen, scene);
-	drawCombatDebugOverlay(engine, screen, scene);
-	drawPathfindingDebugOverlay(engine, screen, scene);
-	drawTimerDebugOverlay(engine, screen, scene);
-}
-
 const ObjectRecord *findSceneObjectByName(const Common::Array<ObjectRecord> &objects,
 		const Common::String &objectName) {
 	for (const ObjectRecord &object : objects) {
@@ -1296,7 +991,7 @@ static void renderQuickTipsScreen(HarvesterEngine &engine, const SceneState &sce
 	if (!screen)
 		return;
 
-	drawRoomScene(engine, *screen, scene, scene.targetPaletteBrightness);
+	RoomPresentation::drawRoomScene(engine, *screen, scene, scene.targetPaletteBrightness);
 	drawQuickTipsPanel(engine, config, layout, tipText);
 
 	if (engine.getRuntimeEntities())
@@ -1475,7 +1170,7 @@ Common::Error Flow::runQuickTips() {
 			return transitionError;
 
 		logScenePaletteSummary("quick tips scene palette", scene, kPaletteBrightnessBlack);
-		drawRoomScene(_engine, *screen, scene, kPaletteBrightnessBlack);
+		RoomPresentation::drawRoomScene(_engine, *screen, scene, kPaletteBrightnessBlack);
 		screen->makeAllDirty();
 		screen->update();
 
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index abe6160033b..8dfd90ec864 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -721,7 +721,7 @@ InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw)
 		if (!activeScreen)
 			return InputResult::leaveRoom(Common::kNoError);
 		IndexedBitmap roomMenuBackdrop;
-		drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
+		RoomPresentation::drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
 		if (!captureScreenBackdrop(*activeScreen, roomMenuBackdrop))
 			return InputResult::leaveRoom(Common::kReadingFailed);
 		Common::Error menuError = _flow.runRoomMenu(
diff --git a/engines/harvester/room_presentation.cpp b/engines/harvester/room_presentation.cpp
index 024c15148c6..1807e9f44f0 100644
--- a/engines/harvester/room_presentation.cpp
+++ b/engines/harvester/room_presentation.cpp
@@ -26,12 +26,16 @@
 #include "common/events.h"
 #include "common/system.h"
 #include "common/textconsole.h"
+#include "graphics/font.h"
+#include "graphics/fontman.h"
 #include "graphics/framelimiter.h"
 #include "harvester/art.h"
 #include "harvester/detection.h"
+#include "harvester/entity.h"
 #include "harvester/flow.h"
 #include "harvester/harvester.h"
 #include "harvester/palette_utils.h"
+#include "harvester/player.h"
 #include "harvester/resources.h"
 #include "harvester/room_support.h"
 
@@ -39,6 +43,13 @@ namespace Harvester {
 
 static const float kNormalPaletteBrightness = 1.0f;
 static const float kDimPaletteBrightness = 0.6f;
+static const char *const kPlayerActorEntityName = "PLAYER";
+static const int kRoomDebugLabelPaddingX = 3;
+static const int kRoomDebugLabelPaddingY = 2;
+static const int kRoomDebugRoomNameX = 4;
+static const int kRoomDebugRoomNameY = 4;
+static const int kTimerDebugOverlayXDivisor = 8;
+static const int kTimerDebugOverlayLineSpacing = 1;
 
 static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *palette) {
 	Common::Array<byte> data;
@@ -56,6 +67,306 @@ static void setScaledRoomPalette(Graphics::Screen &screen, const byte *palette,
 	screen.setPalette(scaledPalette);
 }
 
+static byte findNearestPaletteColor(const byte *palette, byte red, byte green, byte blue) {
+	if (!palette)
+		return 0;
+
+	byte bestIndex = 0;
+	uint32 bestDistance = 0xffffffffu;
+	for (int i = 0; i < 256; ++i) {
+		const int paletteOffset = i * 3;
+		const int dr = (int)palette[paletteOffset] - red;
+		const int dg = (int)palette[paletteOffset + 1] - green;
+		const int db = (int)palette[paletteOffset + 2] - blue;
+		const uint32 distance = (uint32)(dr * dr + dg * dg + db * db);
+		if (distance < bestDistance) {
+			bestDistance = distance;
+			bestIndex = (byte)i;
+		}
+	}
+
+	return bestIndex;
+}
+
+static Common::String resolveRoomDebugObjectLabel(HarvesterEngine &engine, const ObjectRecord &object) {
+	if (Script *script = engine.getScript()) {
+		const Common::String resolvedLabel = script->resolveObjectLabel(object);
+		if (!resolvedLabel.empty())
+			return resolvedLabel;
+	}
+
+	return object.objectName;
+}
+
+static Common::String resolveRoomDebugNpcLabel(const NpcRecord &npc) {
+	Common::String label = !npc.entityInitArg.empty() ? npc.entityInitArg : npc.npcName;
+	for (uint i = 0; i < label.size(); ++i) {
+		if (label[i] == '_')
+			label.setChar(' ', i);
+	}
+
+	return label;
+}
+
+static Common::String resolveCombatDebugLabel(const Common::String &name, int currentHitPoints, int maxHitPoints) {
+	Common::String label = name;
+	for (uint i = 0; i < label.size(); ++i) {
+		if (label[i] == '_')
+			label.setChar(' ', i);
+	}
+
+	return Common::String::format("%s (%d/%d)", label.c_str(), currentHitPoints, maxHitPoints);
+}
+
+static void drawRoomDebugLabel(Graphics::Screen &screen, const Graphics::Font &font,
+		const Common::String &text, int x, int y, byte textColor, byte backgroundColor) {
+	if (text.empty())
+		return;
+
+	const int labelWidth = font.getStringWidth(text) + kRoomDebugLabelPaddingX * 2;
+	const int labelHeight = font.getFontHeight() + kRoomDebugLabelPaddingY * 2;
+	const int drawX = CLIP<int>(x, 0, MAX(0, screen.w - labelWidth));
+	const int drawY = CLIP<int>(y, 0, MAX(0, screen.h - labelHeight));
+	const Common::Rect background(drawX, drawY,
+		MIN<int>(screen.w, drawX + labelWidth), MIN<int>(screen.h, drawY + labelHeight));
+	screen.fillRect(background, backgroundColor);
+	font.drawString(&screen, text,
+		background.left + kRoomDebugLabelPaddingX,
+		background.top + kRoomDebugLabelPaddingY,
+		MAX(0, background.width() - kRoomDebugLabelPaddingX * 2),
+		textColor);
+}
+
+static void drawRoomDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
+		const SceneState &scene) {
+	if (!engine.isRoomDebugEnabled())
+		return;
+
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+	if (!font)
+		return;
+
+	byte displayPalette[256 * 3];
+	screen.getPalette(displayPalette);
+	const byte black = findNearestPaletteColor(displayPalette, 0x00, 0x00, 0x00);
+	const byte white = findNearestPaletteColor(displayPalette, 0xff, 0xff, 0xff);
+	const byte darkGray = findNearestPaletteColor(displayPalette, 0x40, 0x40, 0x40);
+	const byte red = findNearestPaletteColor(displayPalette, 0xff, 0x00, 0x00);
+	Common::String roomName = scene.state.roomName;
+	roomName.toUppercase();
+
+	drawRoomDebugLabel(screen, *font, roomName,
+		kRoomDebugRoomNameX, kRoomDebugRoomNameY, black, white);
+
+	for (const RegionRecord &region : scene.sceneRegions)
+		drawRoomDebugLabel(screen, *font, region.regionName, region.left, region.top, white, darkGray);
+
+	EntityManager *entityManager = engine.getRuntimeEntities();
+	for (const ObjectRecord &object : scene.sceneObjects) {
+		const Entity *entity = entityManager
+			? entityManager->findSceneEntityByName(object.objectName)
+			: nullptr;
+		if (entity && entity->getClassId() == kEntityClassBackground)
+			continue;
+
+		drawRoomDebugLabel(screen, *font, resolveRoomDebugObjectLabel(engine, object),
+			object.currentX, object.currentY, white, black);
+	}
+
+	for (const NpcRecord &npc : scene.state.roomNpcs) {
+		const Entity *entity = entityManager
+			? entityManager->findSceneEntityByName(npc.npcName)
+			: nullptr;
+		if (!entity || entity->getClassId() != kEntityClassNpc || !entity->isVisible())
+			continue;
+
+		const Common::Rect npcRect = entity->getScreenRect();
+		drawRoomDebugLabel(screen, *font, resolveRoomDebugNpcLabel(npc),
+			npcRect.left, npcRect.top, white, red);
+	}
+}
+
+static void drawCombatDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
+		const SceneState &scene) {
+	if (!engine.isCombatDebugEnabled())
+		return;
+
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+	EntityManager *entityManager = engine.getRuntimeEntities();
+	Script *script = engine.getScript();
+	if (!font || !entityManager)
+		return;
+
+	byte displayPalette[256 * 3];
+	screen.getPalette(displayPalette);
+	const byte black = findNearestPaletteColor(displayPalette, 0x00, 0x00, 0x00);
+	const byte white = findNearestPaletteColor(displayPalette, 0xff, 0xff, 0xff);
+	const byte red = findNearestPaletteColor(displayPalette, 0xff, 0x00, 0x00);
+
+	const Entity *playerEntity = entityManager->findSceneEntityByName(kPlayerActorEntityName);
+	if (script && playerEntity &&
+			playerEntity->getClassId() == kEntityClassPlayer &&
+			playerEntity->isVisible()) {
+		const Common::Rect playerRect = playerEntity->getScreenRect();
+		drawRoomDebugLabel(screen, *font,
+			resolveCombatDebugLabel(playerEntity->getName(),
+				script->getPlayerCurrentHitPoints(), Script::kDefaultPlayerHitPoints),
+			playerRect.left, playerRect.top, white, red);
+	}
+
+	for (const MonsterRecord &monster : scene.state.roomMonsters) {
+		const Entity *entity = entityManager->findSceneEntityByName(monster.monsterName);
+		if (!entity || entity->getClassId() != kEntityClassMonster || !entity->isVisible())
+			continue;
+
+		const Common::Rect monsterRect = entity->getScreenRect();
+		drawRoomDebugLabel(screen, *font,
+			resolveCombatDebugLabel(monster.monsterName,
+				monster.currentHitPoints, monster.initialHitPoints),
+			monsterRect.left, monsterRect.top, white, black);
+	}
+}
+
+static Common::Rect getPathfindingMovementBounds(const Graphics::Screen &screen,
+		const RoomSetupState &state) {
+	if (!Player::supportsMovementBand(state))
+		return Common::Rect();
+
+	return Common::Rect(0, state.roomMaxZScreenY, screen.w, state.roomMinZScreenY + 1);
+}
+
+static void drawClippedDebugFrame(Graphics::Screen &screen, Common::Rect rect, byte color) {
+	rect.clip(screen.getBounds());
+	if (rect.isEmpty())
+		return;
+
+	screen.frameRect(rect, color);
+}
+
+static bool canEntityAffectPlayerPathfinding(const Entity &entity) {
+	if (!entity.isVisible())
+		return false;
+
+	switch (entity.getClassId()) {
+	case kEntityClassCursor:
+	case kEntityClassBackground:
+	case kEntityClassPlayer:
+	case kEntityClassRectHotspot:
+	case kEntityClassRectHotspot19:
+	case kEntityClassTimer:
+		return false;
+	default:
+		break;
+	}
+
+	return entity.getClassId() == kEntityClassDisabledHotspot || entity.hasFrames();
+}
+
+static void drawPathfindingBlockerOutline(Graphics::Screen &screen, EntityManager &entityManager,
+		const Common::String &entityName, const Common::Rect &movementBounds, byte color) {
+	const Entity *entity = entityManager.findSceneEntityByName(entityName);
+	if (!entity || !canEntityAffectPlayerPathfinding(*entity))
+		return;
+
+	Common::Rect rect = entity->getScreenRect();
+	if (rect.isEmpty())
+		return;
+
+	Common::Rect overlap = rect;
+	overlap.clip(movementBounds);
+	if (overlap.isEmpty())
+		return;
+
+	drawClippedDebugFrame(screen, rect, color);
+}
+
+static void drawPathfindingDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
+		const SceneState &scene) {
+	if (!engine.isPathfindingDebugEnabled())
+		return;
+
+	byte displayPalette[256 * 3];
+	screen.getPalette(displayPalette);
+	const byte blue = findNearestPaletteColor(displayPalette, 0x00, 0x40, 0xff);
+	const byte green = findNearestPaletteColor(displayPalette, 0x00, 0xff, 0x00);
+	const Common::Rect movementBounds = getPathfindingMovementBounds(screen, scene.state);
+	if (movementBounds.isEmpty())
+		return;
+
+	drawClippedDebugFrame(screen, movementBounds, blue);
+
+	EntityManager *entityManager = engine.getRuntimeEntities();
+	if (!entityManager)
+		return;
+
+	for (const ObjectRecord &object : scene.sceneObjects)
+		drawPathfindingBlockerOutline(screen, *entityManager,
+			object.objectName, movementBounds, green);
+
+	for (const AnimRecord &anim : scene.sceneAnimations)
+		drawPathfindingBlockerOutline(screen, *entityManager,
+			anim.animName, movementBounds, green);
+}
+
+static Common::String resolveTimerDebugLabel(const TimerRecord &timer, const Entity &entity) {
+	const Common::String actionTag = timer.arg2.empty() ? Common::String("<none>") : timer.arg2;
+
+	return Common::String::format("%s start=%d current=%d action=%s",
+		timer.timerName.c_str(), entity.getTimerInitialValue(),
+		entity.getTimerCurrentValue(), actionTag.c_str());
+}
+
+static void drawTimerDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
+		const SceneState &) {
+	if (!engine.isTimerDebugEnabled())
+		return;
+
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+	EntityManager *entityManager = engine.getRuntimeEntities();
+	Script *script = engine.getScript();
+	if (!font || !entityManager || !script)
+		return;
+
+	Common::Array<Common::String> labels;
+	for (const TimerRecord &timer : script->getTimers()) {
+		const Entity *entity = entityManager->findSceneEntityByName(timer.timerName);
+		if (!entity || entity->getClassId() != kEntityClassTimer || !entity->isTimerEnabled())
+			continue;
+
+		const TimerRecord *runtimeTimer = script->findRuntimeTimerRecord(timer.timerName);
+		labels.push_back(resolveTimerDebugLabel(runtimeTimer ? *runtimeTimer : timer, *entity));
+	}
+	if (labels.empty())
+		return;
+
+	byte displayPalette[256 * 3];
+	screen.getPalette(displayPalette);
+	const byte black = findNearestPaletteColor(displayPalette, 0x00, 0x00, 0x00);
+	const byte white = findNearestPaletteColor(displayPalette, 0xff, 0xff, 0xff);
+
+	const int labelHeight = font->getFontHeight() + kRoomDebugLabelPaddingY * 2;
+	const int lineHeight = labelHeight + kTimerDebugOverlayLineSpacing;
+	const int totalHeight = (int)labels.size() * labelHeight +
+		(int)(labels.size() - 1) * kTimerDebugOverlayLineSpacing;
+	const int x = screen.w / kTimerDebugOverlayXDivisor;
+	const int y = CLIP<int>((screen.h - totalHeight) / 2, 0, MAX(0, screen.h - totalHeight));
+
+	for (uint i = 0; i < labels.size(); ++i)
+		drawRoomDebugLabel(screen, *font, labels[i], x, y + (int)i * lineHeight, white, black);
+}
+
+void RoomPresentation::drawRoomScene(HarvesterEngine &engine, Graphics::Screen &screen, const SceneState &scene,
+		float brightness) {
+	setScaledRoomPalette(screen, scene.palette, brightness);
+	screen.fillRect(screen.getBounds(), 0);
+	if (engine.getRuntimeEntities())
+		engine.getRuntimeEntities()->drawSceneEntities(screen);
+	drawRoomDebugOverlay(engine, screen, scene);
+	drawCombatDebugOverlay(engine, screen, scene);
+	drawPathfindingDebugOverlay(engine, screen, scene);
+	drawTimerDebugOverlay(engine, screen, scene);
+}
+
 RoomPresentation::RoomPresentation(HarvesterEngine &engine, SceneState &scene,
 		const Art *art, const Graphics::Font *inspectFont, bool useNativeInspectFont)
 	: _engine(engine), _scene(scene), _art(art), _inspectFont(inspectFont),
diff --git a/engines/harvester/room_presentation.h b/engines/harvester/room_presentation.h
index d5f39737f9d..577da6b4fe3 100644
--- a/engines/harvester/room_presentation.h
+++ b/engines/harvester/room_presentation.h
@@ -63,6 +63,9 @@ public:
 	RoomPresentation(HarvesterEngine &engine, SceneState &scene,
 		const Art *art, const Graphics::Font *inspectFont, bool useNativeInspectFont);
 
+	static void drawRoomScene(HarvesterEngine &engine, Graphics::Screen &screen,
+		const SceneState &scene, float brightness);
+
 	bool captureDialogueBackdrop(IndexedBitmap &dialogueBackdrop);
 	Common::Error applyLightingCommand(StartupLightingCommand lightingCommand);
 	Common::Error runModalShowText(const ResolvedText &modalText, Flow &flow);
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index fc8c0d122fb..c2dd2d51c30 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -73,8 +73,6 @@ Common::Rect getRoomObjectHotspotBounds(const ObjectRecord &object);
 bool shouldDisplaySceneAnimation(const AnimRecord &anim);
 bool loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resources, SceneState &scene);
 bool shouldRunStartupRoomProbe();
-void drawRoomScene(HarvesterEngine &engine, Graphics::Screen &screen, const SceneState &scene,
-	float brightness);
 const ObjectRecord *findSceneObjectByName(const Common::Array<ObjectRecord> &objects,
 	const Common::String &objectName);
 const RegionRecord *findSceneRegionByName(const Common::Array<RegionRecord> &regions,
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index d90da2d6beb..7aa923c77fd 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -488,7 +488,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 				: hoverState.cursorSequence);
 	}
 
-	drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
+	RoomPresentation::drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
 	_combatEffects.drawCombatDamagePopups(*activeScreen, _engine.isCombatDebugEnabled());
 	if (_engine.getScript() && playerState.entity) {
 		drawCombatLoadoutResourceIcons(*activeScreen, *_view.art, *_engine.getScript(),
@@ -1061,7 +1061,7 @@ Common::Error SceneRunner::runRoomLoop(Flow &flow, const Common::String &targetN
 			return transitionError;
 
 		logScenePaletteSummary("room setup initial palette", scene, 0.0f);
-		drawRoomScene(_engine, *screen, scene, 0.0f);
+		RoomPresentation::drawRoomScene(_engine, *screen, scene, 0.0f);
 		screen->makeAllDirty();
 		screen->update();
 


Commit: 30996893141fcab19f737325026a0347725b9451
    https://github.com/scummvm/scummvm/commit/30996893141fcab19f737325026a0347725b9451
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Replace Flow access bridge with explicit operations

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/dialogue.cpp
    engines/harvester/flow.h
    engines/harvester/harvester.cpp
    engines/harvester/input.cpp
    engines/harvester/menu.cpp
    engines/harvester/scene.cpp


diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 48b0992f189..a67dc6c09ac 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -92,29 +92,6 @@
 
 namespace Harvester {
 
-class DialogueFlowAccess {
-public:
-	static bool handleSystemEvent(Flow &flow, const Common::Event &event, Common::Error &result) {
-		return flow.handleSystemEvent(event, result);
-	}
-
-	static void executeStartupAudioCommands(Flow &flow, const Common::Array<AudioCommand> &commands) {
-		flow.executeStartupAudioCommands(commands);
-	}
-
-	static void queueDialogueInteraction(Flow &flow, const InteractionResult &interaction) {
-		flow.queueDialogueInteraction(interaction);
-	}
-
-	static void requestGameOverReturn(Flow &flow) {
-		flow.requestGameOverReturn();
-	}
-
-	static const MenuTextConfig &getMenuTextConfig(const Flow &flow) {
-		return flow._menuTextConfig;
-	}
-};
-
 namespace {
 
 static const int kDialogueOverlayX = 167;
@@ -407,7 +384,7 @@ public:
 			const IndexedBitmap &backdrop, const byte *palette, float paletteBrightness,
 			const NpcRecord &npc)
 		: _engine(engine), _mousePos(mousePos), _flow(flow),
-		  _menuTextConfig(DialogueFlowAccess::getMenuTextConfig(flow)), _backdrop(backdrop),
+		  _menuTextConfig(flow.getMenuTextConfig()), _backdrop(backdrop),
 		  _palette(palette), _paletteBrightness(paletteBrightness), _npc(npc),
 		  _script(engine.getScript()), _text(engine.getText()), _art(engine.getArt()),
 		  _entityManager(engine.getRuntimeEntities()),
@@ -512,7 +489,7 @@ public:
 			Common::Event event;
 			while (g_system->getEventManager()->pollEvent(event)) {
 				Common::Error result = Common::kNoError;
-				if (DialogueFlowAccess::handleSystemEvent(_flow, event, result)) {
+				if (_flow.handleSystemEvent(event, result)) {
 					_engine.stopSpeech();
 					return result;
 				}
@@ -548,7 +525,7 @@ public:
 				Common::Event event;
 				while (g_system->getEventManager()->pollEvent(event)) {
 					Common::Error result = Common::kNoError;
-					if (DialogueFlowAccess::handleSystemEvent(_flow, event, result))
+					if (_flow.handleSystemEvent(event, result))
 						return result;
 
 					if (isDialogueContinueEvent(event))
@@ -638,7 +615,7 @@ public:
 			Common::Event event;
 			while (g_system->getEventManager()->pollEvent(event)) {
 				Common::Error result = Common::kNoError;
-				if (DialogueFlowAccess::handleSystemEvent(_flow, event, result))
+				if (_flow.handleSystemEvent(event, result))
 					return result;
 				if (event.type == Common::EVENT_MOUSEMOVE)
 					_mousePos = event.mouse;
@@ -663,7 +640,7 @@ public:
 
 				while (g_system->getEventManager()->pollEvent(event)) {
 					Common::Error result = Common::kNoError;
-					if (DialogueFlowAccess::handleSystemEvent(_flow, event, result))
+					if (_flow.handleSystemEvent(event, result))
 						return result;
 
 					switch (event.type) {
@@ -741,7 +718,7 @@ public:
 			Common::Event event;
 			while (g_system->getEventManager()->pollEvent(event)) {
 				Common::Error result = Common::kNoError;
-				if (DialogueFlowAccess::handleSystemEvent(_flow, event, result)) {
+				if (_flow.handleSystemEvent(event, result)) {
 					if (editingOther && _entityManager)
 						_entityManager->showCursor();
 					return result;
@@ -847,7 +824,7 @@ public:
 		// GOG run_game_over_screen (0x7c540) enters run_main_menu (0x67390)
 		// with GAMEOVER.BM and GAMEOVER.PAL retained as the menu backdrop.
 		debugC(1, kDebugDialogue, "Harvester: dialogue death requesting game-over menu");
-		DialogueFlowAccess::requestGameOverReturn(_flow);
+		_flow.requestGameOverReturn();
 		return Common::kNoError;
 	}
 
@@ -888,7 +865,7 @@ public:
 				interaction.lightingCommand != kStartupLightingCommandNone ||
 				interaction.requestPlayerGotoXZ ||
 				!interaction.audioCommands.empty()) {
-			DialogueFlowAccess::queueDialogueInteraction(_flow, interaction);
+			_flow.queueDialogueInteraction(interaction);
 		}
 	}
 
@@ -896,7 +873,7 @@ public:
 		if (!interaction.musicPath.empty())
 			(void)_engine.playMusic(interaction.musicPath);
 		if (!interaction.audioCommands.empty())
-			DialogueFlowAccess::executeStartupAudioCommands(_flow, interaction.audioCommands);
+			_flow.executeStartupAudioCommands(interaction.audioCommands);
 		interaction.musicPath.clear();
 		interaction.audioCommands.clear();
 	}
@@ -930,7 +907,7 @@ private:
 			Common::Event event;
 			while (g_system->getEventManager()->pollEvent(event)) {
 				Common::Error result = Common::kNoError;
-				if (DialogueFlowAccess::handleSystemEvent(_flow, event, result))
+				if (_flow.handleSystemEvent(event, result))
 					return result;
 			}
 
@@ -1231,7 +1208,7 @@ private:
 			Common::Event event;
 			while (g_system->getEventManager()->pollEvent(event)) {
 				Common::Error result = Common::kNoError;
-				if (DialogueFlowAccess::handleSystemEvent(_flow, event, result))
+				if (_flow.handleSystemEvent(event, result))
 					return result;
 
 				if (event.type == Common::EVENT_KEYDOWN) {
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index 804c01862ea..9fc4999adac 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -40,7 +40,6 @@ struct Event;
 namespace Harvester {
 
 class HarvesterEngine;
-class DialogueFlowAccess;
 class Interaction;
 struct IndexedBitmap;
 
@@ -55,11 +54,16 @@ public:
 	Common::Error showCdChangePrompt(int discNumber);
 	bool handleSystemEvent(const Common::Event &event, Common::Error &result);
 
+	const MenuTextConfig &getMenuTextConfig() const { return _menuTextConfig; }
+	void executeStartupAudioCommands(const Common::Array<AudioCommand> &commands);
+	void queueDialogueInteraction(const InteractionResult &interaction);
+	void requestGameOverReturn();
+
+	bool hasQueuedDebugInteraction() const { return _hasQueuedDebugInteraction; }
+	bool queueDebugInteraction(const InteractionResult &interaction);
+	bool requestDebugRoomChange(const Common::String &roomName);
+
 private:
-	friend class HarvesterEngine;
-	friend class DialogueFlowAccess;
-	friend class DialogueSystem;
-	friend class InventorySystem;
 	friend class MenuSystem;
 	friend class Interaction;
 	friend class SceneRunner;
@@ -90,18 +94,13 @@ private:
 	Common::Error fadePalette(const byte *palette, float fromBrightness, float toBrightness);
 	bool waitForPaletteFadeTick(uint32 startTick, Common::Error &result);
 	bool pumpTransitionEvents(Common::Error &result);
-	void executeStartupAudioCommands(const Common::Array<AudioCommand> &commands);
-	void queueDialogueInteraction(const InteractionResult &interaction);
 	bool takeQueuedDialogueInteraction(InteractionResult &interaction);
-	bool hasQueuedDebugInteraction() const { return _hasQueuedDebugInteraction; }
-	bool queueDebugInteraction(const InteractionResult &interaction);
 	bool takeQueuedDebugInteraction(InteractionResult &interaction);
 	void prepareForNewGame();
 	void requestNewGameRestart();
 	bool hasPendingNewGameRestart() const;
 	bool takePendingNewGameRestart();
 	void clearPendingNewGameRestart();
-	void requestGameOverReturn();
 	bool hasPendingGameOverReturn() const;
 	bool takePendingGameOverReturn();
 	void clearPendingGameOverReturn();
@@ -113,7 +112,6 @@ private:
 	bool hasPendingCloseupParentRestart() const;
 	bool takePendingCloseupParentRestart();
 	void clearPendingCloseupParentRestart();
-	bool requestDebugRoomChange(const Common::String &roomName);
 	bool hasPendingDebugRoomChange() const { return !_pendingDebugRoomName.empty(); }
 	Common::String takePendingDebugRoomChange();
 	void resetRoomNpcDialogueState();
diff --git a/engines/harvester/harvester.cpp b/engines/harvester/harvester.cpp
index 0f2e1ed503b..6deb9bf7309 100644
--- a/engines/harvester/harvester.cpp
+++ b/engines/harvester/harvester.cpp
@@ -358,7 +358,7 @@ Common::Error HarvesterEngine::run() {
 	if (!flow.load())
 		return Common::kReadingFailed;
 
-	if (!_media->loadQuickTipsResources(flow._menuTextConfig.hasQuickTipsHeader()))
+	if (!_media->loadQuickTipsResources(flow.getMenuTextConfig().hasQuickTipsHeader()))
 		return Common::kReadingFailed;
 
 	// If a savegame was selected from the launcher, load it
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index 8dfd90ec864..5cfe1c017e2 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -136,7 +136,7 @@ void Input::capturePlayerAttackTarget() {
 
 	const RoomHoverState hoverState = resolveRoomHoverState(
 		_engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs, _scene.sceneRegions,
-		_flow._mousePos, _flow._menuTextConfig, &_flow._dialogue);
+		_flow._mousePos, _flow.getMenuTextConfig(), &_flow._dialogue);
 	_session.capturePlayerAttackTarget(_flow._mousePos, hoverState.npc);
 }
 
@@ -353,7 +353,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 					inventoryHover->object.objectName.c_str());
 				_inventory.selectItem(inventoryHover->object.objectName);
 				_inventory.setPromptText(_inventory.buildSelectedPrompt(
-					Common::String(), _flow._menuTextConfig));
+					Common::String(), _flow.getMenuTextConfig()));
 				needsRedraw = true;
 				return InputResult();
 			}
@@ -384,7 +384,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 
 	const RoomHoverState hoverState = resolveRoomHoverState(
 		_engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs, _scene.sceneRegions,
-		_flow._mousePos, _flow._menuTextConfig, &_flow._dialogue);
+		_flow._mousePos, _flow.getMenuTextConfig(), &_flow._dialogue);
 	RoomHoverState clickHoverState = hoverState;
 	const Entity *itemTarget = nullptr;
 	if (_roomItems.hasSelectedItem()) {
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 7c6c1f8e91c..f8d118abb84 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -899,7 +899,7 @@ Common::Error MenuSystem::runMainMenu(Flow &flow) {
 			selectedItem, item.c_str());
 
 		if (selectedItem == kMainMenuItemNewGame) {
-			const MenuTextConfig &config = flow._menuTextConfig;
+			const MenuTextConfig &config = flow.getMenuTextConfig();
 
 			IndexedBitmap menuBackdrop;
 			if (!captureMenuBackdrop(menuBackdrop))
@@ -1151,7 +1151,7 @@ Common::Error MenuSystem::runRoomMenu(const IndexedBitmap &backdrop, const byte
 			selectedItem, item.c_str());
 
 		if (selectedItem == kMainMenuItemNewGame) {
-			const MenuTextConfig &config = flow._menuTextConfig;
+			const MenuTextConfig &config = flow.getMenuTextConfig();
 
 			bool confirmed = false;
 			Common::Error confirmError = runConfirmPrompt(
@@ -1661,7 +1661,7 @@ Common::Error MenuSystem::runConfirmPrompt(const IndexedBitmap &backdrop, const
 	if (!promptFont.isValid() || !choiceFont.isValid())
 		return Common::kReadingFailed;
 
-	const MenuTextConfig &config = flow._menuTextConfig;
+	const MenuTextConfig &config = flow.getMenuTextConfig();
 	const IndexedBitmap *textbox = art->getTextboxBitmap(3);
 	if (!textbox || !textbox->isValid())
 		return Common::kReadingFailed;
@@ -1731,7 +1731,7 @@ Common::Error MenuSystem::runConfirmPrompt(const IndexedBitmap &backdrop, const
 
 Common::Error MenuSystem::runQuitGameConfirm(const IndexedBitmap &backdrop, const byte *palette,
 		float paletteBrightness, Flow &flow) {
-	const MenuTextConfig &config = flow._menuTextConfig;
+	const MenuTextConfig &config = flow.getMenuTextConfig();
 
 	bool confirmed = false;
 	Common::Error confirmError = runConfirmPrompt(
@@ -1772,7 +1772,7 @@ Common::Error MenuSystem::runPasswordPrompt(const IndexedBitmap &backdrop, const
 	while (!_engine.shouldQuit()) {
 		if (needsRedraw) {
 			renderPasswordPromptScreen(_engine, backdrop, palette, paletteBrightness,
-				titleFont, entryFont, *art, flow._menuTextConfig.enterPasswordLabel,
+				titleFont, entryFont, *art, flow.getMenuTextConfig().enterPasswordLabel,
 				password, cursorVisible, drawLogo);
 			needsRedraw = false;
 		}
@@ -1882,7 +1882,7 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 	if (!selectedFont.isValid() || !unselectedFont.isValid())
 		return Common::kReadingFailed;
 
-	const MenuTextConfig &config = flow._menuTextConfig;
+	const MenuTextConfig &config = flow.getMenuTextConfig();
 
 	IndexedBitmap volumeBar;
 	IndexedBitmap indicator;
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index 7aa923c77fd..df08527a01a 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -426,7 +426,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 	RoomHoverState hoverState = suppressHover
 		? RoomHoverState()
 		: resolveRoomHoverState(_engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs,
-			_scene.sceneRegions, _flow._mousePos, _flow._menuTextConfig, &_flow._dialogue);
+			_scene.sceneRegions, _flow._mousePos, _flow.getMenuTextConfig(), &_flow._dialogue);
 	Common::String promptText;
 	Common::String inventoryTooltipText;
 	auto resolveCarryTargetLabel = [&]() {
@@ -458,11 +458,11 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 			targetLabel = resolveCarryTargetLabel();
 		}
 		if (inventorySelectionActive) {
-			promptText = _inventory.buildSelectedPrompt(targetLabel, _flow._menuTextConfig);
+			promptText = _inventory.buildSelectedPrompt(targetLabel, _flow.getMenuTextConfig());
 			_inventory.setPromptText(promptText);
 		} else {
 			promptText = buildUseItemPrompt(
-				_flow._menuTextConfig, _roomItems.getCarriedRoomItemLabel(), targetLabel);
+				_flow.getMenuTextConfig(), _roomItems.getCarriedRoomItemLabel(), targetLabel);
 		}
 		hoverState.cursorSequence = 7;
 	} else if (_inventory.isOpen()) {
@@ -497,7 +497,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 	if (_inventory.isOpen()) {
 		_inventory.drawOverlay(*activeScreen);
 		drawInventoryWeekday(*activeScreen, *_view.inventoryTooltipFont,
-			_inventory.resolveWeekdayLabel(_flow._menuTextConfig));
+			_inventory.resolveWeekdayLabel(_flow.getMenuTextConfig()));
 	}
 	if (inventorySelectionActive) {
 		_inventory.drawSelectedDragItem(*activeScreen, _flow._mousePos);
@@ -688,7 +688,7 @@ InputResult Scene::runRoomFrames(const Common::String &currentRoomTarget) {
 	captureCurrentSaveState();
 
 	if (shouldRunStartupRoomProbe())
-		logStartupRoomProbe(_engine, _scene, currentRoomTarget, _flow._mousePos, _flow._menuTextConfig);
+		logStartupRoomProbe(_engine, _scene, currentRoomTarget, _flow._mousePos, _flow.getMenuTextConfig());
 
 	while (!_engine.shouldQuit()) {
 		if (_flow.hasPendingMainMenuReturn())


Commit: 704302dcdc8b329dd4da3046d28452b8e1d460be
    https://github.com/scummvm/scummvm/commit/704302dcdc8b329dd4da3046d28452b8e1d460be
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Encapsulate remaining Flow state access

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/flow.cpp
    engines/harvester/flow.h
    engines/harvester/input.cpp
    engines/harvester/menu.cpp
    engines/harvester/room_support.h
    engines/harvester/scene.cpp


diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index b9de7ecbd8d..e88d6ed5eee 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -814,6 +814,11 @@ RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupSta
 	return hoverState;
 }
 
+RoomHoverState Flow::getRoomHoverState(const SceneState &scene) const {
+	return resolveRoomHoverState(_engine, scene.state, scene.sceneObjects,
+		scene.state.roomNpcs, scene.sceneRegions, _mousePos, _menuTextConfig, &_dialogue);
+}
+
 static bool findRoomObjectProbePoint(HarvesterEngine &engine, const Common::Array<ObjectRecord> &sceneObjects,
 		const ObjectRecord &object, Common::Point &probePoint) {
 	const Common::Rect bounds = getRoomObjectHotspotBounds(object);
@@ -852,11 +857,9 @@ static bool findRoomObjectProbePoint(HarvesterEngine &engine, const Common::Arra
 	return false;
 }
 
-void logStartupRoomProbe(HarvesterEngine &engine, const SceneState &scene,
-		const Common::String &entranceName, Common::Point &mousePos,
-		const MenuTextConfig &menuTextConfig) {
-	EntityManager *entityManager = engine.getRuntimeEntities();
-	Script *script = engine.getScript();
+void Flow::logStartupRoomProbe(const SceneState &scene, const Common::String &entranceName) {
+	EntityManager *entityManager = _engine.getRuntimeEntities();
+	Script *script = _engine.getScript();
 	if (!entityManager || !script)
 		return;
 
@@ -890,19 +893,19 @@ void logStartupRoomProbe(HarvesterEngine &engine, const SceneState &scene,
 			continue;
 
 		Common::Point probePoint;
-		if (!findRoomObjectProbePoint(engine, scene.sceneObjects, object, probePoint))
+		if (!findRoomObjectProbePoint(_engine, scene.sceneObjects, object, probePoint))
 			continue;
 
-		const ObjectRecord *hoveredObject = findRoomObjectAtPoint(engine, scene.sceneObjects, probePoint);
+		const ObjectRecord *hoveredObject = findRoomObjectAtPoint(_engine, scene.sceneObjects, probePoint);
 		if (!hoveredObject)
 			continue;
 
-		mousePos = probePoint;
+		_mousePos = probePoint;
 		const Common::String objectLabel = script->resolveObjectLabel(*hoveredObject);
 		ResolvedText inspectText;
 		const RoomHoverState hoverState = resolveRoomHoverState(
-			engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
-			probePoint, menuTextConfig);
+			_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
+			probePoint, _menuTextConfig);
 		const bool hasInteraction = script->hasObjectInteraction(*hoveredObject);
 		const bool hasInspectText = script->resolveObjectInspectText(*hoveredObject, inspectText);
 		debugC(1, kDebugRoom,
@@ -918,8 +921,8 @@ void logStartupRoomProbe(HarvesterEngine &engine, const SceneState &scene,
 			for (int y = scene.state.roomMaxZScreenY; y <= scene.state.roomMinZScreenY && !foundFloorProbe; y += 12) {
 				for (int x = 48; x < 592; x += 16) {
 					const RoomHoverState candidateHover = resolveRoomHoverState(
-						engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
-						Common::Point(x, y), menuTextConfig);
+						_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
+						Common::Point(x, y), _menuTextConfig);
 					if (candidateHover.cursorSequence == kCursorSequenceWalk) {
 						floorProbe = Common::Point(x, y);
 						foundFloorProbe = true;
@@ -929,8 +932,8 @@ void logStartupRoomProbe(HarvesterEngine &engine, const SceneState &scene,
 			}
 
 			const RoomHoverState floorHover = foundFloorProbe
-				? resolveRoomHoverState(engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
-					scene.sceneRegions, floorProbe, menuTextConfig)
+				? resolveRoomHoverState(_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
+					scene.sceneRegions, floorProbe, _menuTextConfig)
 				: RoomHoverState();
 			debugC(1, kDebugRoom,
 				"Harvester: startup probe floor room='%s' point=(%d,%d) found=%d cursor_sequence=%d prompt='%s'",
@@ -943,7 +946,7 @@ void logStartupRoomProbe(HarvesterEngine &engine, const SceneState &scene,
 				probePlayer.initialize(scene.state, player, 0);
 				probePlayer.setMoveTargetFromScreenPoint(scene.state, floorProbe.x, floorProbe.y);
 				for (int i = 0; i < 32 && probePlayer.getState().hasMoveTarget; ++i)
-					(void)probePlayer.stepMoveTarget(engine, scene.state, scene.sceneObjects, scene.sceneAnimations);
+					(void)probePlayer.stepMoveTarget(_engine, scene.state, scene.sceneObjects, scene.sceneAnimations);
 
 				const Common::Rect movedRect = player->getScreenRect();
 				debugC(1, kDebugPlayer,
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index 9fc4999adac..35e34459728 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -55,6 +55,14 @@ public:
 	bool handleSystemEvent(const Common::Event &event, Common::Error &result);
 
 	const MenuTextConfig &getMenuTextConfig() const { return _menuTextConfig; }
+	const Common::Point &getMousePos() const { return _mousePos; }
+	uint getQuickTipCount() const { return _quickTips.size(); }
+	const Common::String &getQuickTip(uint index) const { return _quickTips[index]; }
+	bool hasRoomNpcHandler(const Common::String &npcName) const { return _dialogue.hasRoomNpcHandler(npcName); }
+	bool isNestedRoomLoop() const { return _roomLoopDepth > 1; }
+	RoomHoverState getRoomHoverState(const SceneState &scene) const;
+	void logStartupRoomProbe(const SceneState &scene, const Common::String &entranceName);
+
 	void executeStartupAudioCommands(const Common::Array<AudioCommand> &commands);
 	void queueDialogueInteraction(const InteractionResult &interaction);
 	void requestGameOverReturn();
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index 5cfe1c017e2..6029618b424 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -134,18 +134,16 @@ void Input::capturePlayerAttackTarget() {
 
 	player.clearAttackTarget();
 
-	const RoomHoverState hoverState = resolveRoomHoverState(
-		_engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs, _scene.sceneRegions,
-		_flow._mousePos, _flow.getMenuTextConfig(), &_flow._dialogue);
-	_session.capturePlayerAttackTarget(_flow._mousePos, hoverState.npc);
+	const RoomHoverState hoverState = _flow.getRoomHoverState(_scene);
+	_session.capturePlayerAttackTarget(_flow.getMousePos(), hoverState.npc);
 }
 
 void Input::handleMouseMove(bool &needsRedraw) {
 	if (_inventory.isOpen() && _inventory.hasSelection()) {
-		const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
+		const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
 		// Native inventory handoff is driven by the latched selected item, not by
 		// a second click after the selection has already been made.
-		if (!inventoryHover && isOutsideNativeInventoryDragCloseBounds(_flow._mousePos)) {
+		if (!inventoryHover && isOutsideNativeInventoryDragCloseBounds(_flow.getMousePos())) {
 			if (_inventory.close())
 				needsRedraw = true;
 		}
@@ -168,8 +166,8 @@ InputResult Input::handleRightButtonDown(bool &needsRedraw) {
 		return InputResult();
 	_session.clearPendingRegionInteraction();
 	if (_inventory.isOpen()) {
-		const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
-		debugLogInventoryClick("right", _flow._mousePos, inventoryHover);
+		const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
+		debugLogInventoryClick("right", _flow.getMousePos(), inventoryHover);
 		if (_inventory.hasSelection()) {
 			debugC(1, kDebugInventory,
 				"Harvester: inventory right click cleared active selection='%s'",
@@ -277,7 +275,7 @@ InputResult Input::handleRightButtonDown(bool &needsRedraw) {
 		player.cancelMovementForAttack();
 		(void)player.syncCombatLoadoutVisual(_engine, _scene.state, script->getPlayerCombatLoadout());
 		capturePlayerAttackTarget();
-		if (player.startAttackAnimation(_scene.state, _flow._mousePos)) {
+		if (player.startAttackAnimation(_scene.state, _flow.getMousePos())) {
 			debugC(1, kDebugCombat,
 				"Harvester: combat player attack start loadout=%d weapon='%s' damage=%d damage_type='%s' target_class='%s' target='%s' cursor=(%d,%d)",
 				playerState.combatLoadout,
@@ -285,7 +283,7 @@ InputResult Input::handleRightButtonDown(bool &needsRedraw) {
 				Player::resolveCombatLoadoutDamageAmount(playerState.combatLoadout),
 				Player::describeCombatDamageType(Player::resolveCombatLoadoutDamageType(playerState.combatLoadout)),
 				describeCombatTargetClass(playerState.attackTargetClassId),
-				playerState.attackTargetName.c_str(), _flow._mousePos.x, _flow._mousePos.y);
+				playerState.attackTargetName.c_str(), _flow.getMousePos().x, _flow.getMousePos().y);
 			needsRedraw = true;
 		} else {
 			player.clearAttackTarget();
@@ -333,8 +331,8 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 
 	const Common::Rect inventoryPanelBounds = _inventory.getPanelBounds();
 	if (_inventory.isOpen()) {
-		const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
-		debugLogInventoryClick("left", _flow._mousePos, inventoryHover);
+		const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
+		debugLogInventoryClick("left", _flow.getMousePos(), inventoryHover);
 		if (inventoryHover) {
 			if (InventorySystem::isExitObject(inventoryHover->object)) {
 				debugC(1, kDebugInventory, "Harvester: inventory left click exit button");
@@ -373,29 +371,27 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 		}
 
 		if (_inventory.hasSelection()) {
-			if (!inventoryPanelBounds.contains(_flow._mousePos) && _inventory.close())
+			if (!inventoryPanelBounds.contains(_flow.getMousePos()) && _inventory.close())
 				needsRedraw = true;
 			return InputResult();
 		}
-		if (!inventoryPanelBounds.contains(_flow._mousePos) && _inventory.close())
+		if (!inventoryPanelBounds.contains(_flow.getMousePos()) && _inventory.close())
 			needsRedraw = true;
 		return InputResult();
 	}
 
-	const RoomHoverState hoverState = resolveRoomHoverState(
-		_engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs, _scene.sceneRegions,
-		_flow._mousePos, _flow.getMenuTextConfig(), &_flow._dialogue);
+	const RoomHoverState hoverState = _flow.getRoomHoverState(_scene);
 	RoomHoverState clickHoverState = hoverState;
 	const Entity *itemTarget = nullptr;
 	if (_roomItems.hasSelectedItem()) {
 		clickHoverState = RoomHoverState();
-		itemTarget = _roomItems.findSelectedItemTarget(_flow._mousePos);
+		itemTarget = _roomItems.findSelectedItemTarget(_flow.getMousePos());
 		if (!itemTarget)
 			return InputResult();
 		if (itemTarget == playerState.entity)
 			clickHoverState.playerEntity = itemTarget;
 		if (itemTarget->getClassId() == kEntityClassNpc &&
-				_flow._dialogue.hasRoomNpcHandler(itemTarget->getName()) &&
+				_flow.hasRoomNpcHandler(itemTarget->getName()) &&
 				_engine.getScript()->isNamedNpcDeathTypeClear(itemTarget->getName())) {
 			for (const NpcRecord &npc : _scene.state.roomNpcs) {
 				if (npc.npcName.equalsIgnoreCase(itemTarget->getName())) {
@@ -408,7 +404,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 	}
 	debugC(1, kDebugRoom,
 		"Harvester: room click room='%s' mouse=(%d,%d) object='%s' npc='%s' region='%s' cursor_sequence=%d prompt='%s'",
-		_scene.state.roomName.c_str(), _flow._mousePos.x, _flow._mousePos.y,
+		_scene.state.roomName.c_str(), _flow.getMousePos().x, _flow.getMousePos().y,
 		clickHoverState.object ? clickHoverState.object->objectName.c_str() : "",
 		clickHoverState.npc ? clickHoverState.npc->npcName.c_str() : "",
 		clickHoverState.region ? clickHoverState.region->regionName.c_str() : "",
@@ -497,7 +493,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 			needsRedraw = true;
 			return InputResult();
 		}
-		_interactionProcessor.queueRegionInteraction(*hoverState.region, _flow._mousePos);
+		_interactionProcessor.queueRegionInteraction(*hoverState.region, _flow.getMousePos());
 		Common::Error interactionError =
 			_interactionProcessor.tryActivatePendingRegion(_inventory, needsRedraw);
 		if (interactionError.getCode() != Common::kNoError)
@@ -515,17 +511,17 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 				const Common::Rect playerRect = playerState.entity->getScreenRect();
 				debugC(1, kDebugPathfinding,
 					"Harvester: pathfinding click accepted room='%s' mouse=(%d,%d) movement_band=(y=%d..%d,z=%d..%d) player=(%d,%d,z=%.2f) player_rect=(%d,%d)-(%d,%d)",
-					_scene.state.roomName.c_str(), _flow._mousePos.x, _flow._mousePos.y,
+					_scene.state.roomName.c_str(), _flow.getMousePos().x, _flow.getMousePos().y,
 					_scene.state.roomMaxZScreenY, _scene.state.roomMinZScreenY,
 					_scene.state.roomMinZ, _scene.state.roomMaxZ,
 					playerState.centerX, playerState.bottomY, (double)playerState.z,
 					playerRect.left, playerRect.top, playerRect.right, playerRect.bottom);
 			}
-			player.setMoveTargetFromScreenPoint(_scene.state, _flow._mousePos.x, _flow._mousePos.y);
+			player.setMoveTargetFromScreenPoint(_scene.state, _flow.getMousePos().x, _flow.getMousePos().y);
 			if (_engine.isPathfindingDebugEnabled()) {
 				debugC(1, kDebugPathfinding,
 					"Harvester: pathfinding click target room='%s' mouse=(%d,%d) target=(%d,%d,z=%.2f)",
-					_scene.state.roomName.c_str(), _flow._mousePos.x, _flow._mousePos.y,
+					_scene.state.roomName.c_str(), _flow.getMousePos().x, _flow.getMousePos().y,
 					playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ);
 			}
 			needsRedraw = true;
@@ -688,7 +684,7 @@ InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw)
 			key.keycode == Common::KEYCODE_RCTRL) {
 		inputState.attackModifierHeld = true;
 	} else if (key.keycode == Common::KEYCODE_k && _engine.isCombatDebugEnabled()) {
-		Common::Error debugKillError = _session.killDebugActiveMonster(_flow._mousePos, needsRedraw);
+		Common::Error debugKillError = _session.killDebugActiveMonster(_flow.getMousePos(), needsRedraw);
 		if (debugKillError.getCode() != Common::kNoError)
 			return InputResult::leaveRoom(debugKillError);
 		return InputResult();
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index f8d118abb84..e2653e448c2 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -1896,7 +1896,7 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 	int capturedSlider = -1;
 	bool showingQuickTips = false;
 	bool needsRedraw = true;
-	uint quickTipIndex = flow._quickTips.empty() ? 0 : _engine.getRandomNumber(flow._quickTips.size() - 1);
+	uint quickTipIndex = flow.getQuickTipCount() == 0 ? 0 : _engine.getRandomNumber(flow.getQuickTipCount() - 1);
 	QuickTipsLayout quickTipsLayout;
 
 	auto persistConfig = [&]() -> bool {
@@ -2009,7 +2009,7 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 			toggleGore();
 			break;
 		case 5:
-			if (!flow._quickTips.empty()) {
+			if (flow.getQuickTipCount() != 0) {
 				if (!resolveQuickTipsLayout(_engine, config, quickTipsLayout))
 					return Common::kReadingFailed;
 				showingQuickTips = true;
@@ -2031,7 +2031,7 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 		if (needsRedraw) {
 			if (showingQuickTips) {
 				renderQuickTipsOverlay(_engine, backdrop, palette, paletteBrightness,
-					config, quickTipsLayout, flow._quickTips[quickTipIndex]);
+					config, quickTipsLayout, flow.getQuickTip(quickTipIndex));
 			} else {
 				renderOptionsMenuScreen(_engine, backdrop, palette, paletteBrightness,
 					selectedFont, unselectedFont, *art, config, volumeBar, indicator, selectedItem);
@@ -2055,7 +2055,7 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 						showingQuickTips = false;
 						needsRedraw = true;
 					} else if (quickTipsLayout.nextRect.contains(_mousePos)) {
-						quickTipIndex = (quickTipIndex + 1) % flow._quickTips.size();
+						quickTipIndex = (quickTipIndex + 1) % flow.getQuickTipCount();
 						needsRedraw = true;
 					} else if (quickTipsLayout.toggleRect.contains(_mousePos)) {
 						script->setQuickTipsEnabled(!script->isQuickTipsEnabled());
@@ -2174,7 +2174,7 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 					break;
 				case Common::KEYCODE_q:
 					selectedItem = 5;
-					if (!flow._quickTips.empty()) {
+					if (flow.getQuickTipCount() != 0) {
 						showingQuickTips = true;
 						needsRedraw = true;
 					}
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index c2dd2d51c30..b8b110a4a0a 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -92,9 +92,6 @@ RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupSta
 	const Common::Array<RegionRecord> &regions,
 	const Common::Point &mousePos, const MenuTextConfig &menuTextConfig,
 	const DialogueSystem *dialogue = nullptr);
-void logStartupRoomProbe(HarvesterEngine &engine, const SceneState &scene,
-	const Common::String &entranceName, Common::Point &mousePos,
-	const MenuTextConfig &menuTextConfig);
 
 } // End of namespace Harvester
 
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index df08527a01a..9d59fd9fde7 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -419,18 +419,17 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 	const bool inventorySelectionActive = _inventory.hasSelection();
 	const bool roomCarryActive = _roomItems.hasCarriedRoomItem();
 	const bool activeCarry = inventorySelectionActive || roomCarryActive;
-	const bool inventoryPanelContainsMouse = _inventory.isOpen() && inventoryPanelBounds.contains(_flow._mousePos);
+	const bool inventoryPanelContainsMouse = _inventory.isOpen() && inventoryPanelBounds.contains(_flow.getMousePos());
 	const bool suppressHover = _presentationState.showingInspectText || idleState.active || idleState.exiting ||
 		isPlayerCombatLocked() ||
 		(_inventory.isOpen() && (inventoryPanelContainsMouse || !inventorySelectionActive));
 	RoomHoverState hoverState = suppressHover
 		? RoomHoverState()
-		: resolveRoomHoverState(_engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs,
-			_scene.sceneRegions, _flow._mousePos, _flow.getMenuTextConfig(), &_flow._dialogue);
+		: _flow.getRoomHoverState(_scene);
 	Common::String promptText;
 	Common::String inventoryTooltipText;
 	auto resolveCarryTargetLabel = [&]() {
-		const Entity *target = suppressHover ? nullptr : _roomItems.findSelectedItemTarget(_flow._mousePos, false);
+		const Entity *target = suppressHover ? nullptr : _roomItems.findSelectedItemTarget(_flow.getMousePos(), false);
 		if (!target)
 			return Common::String();
 		if (target == playerState.entity)
@@ -445,7 +444,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 	};
 	if (activeCarry) {
 		Common::String targetLabel;
-		const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
+		const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
 		if (_inventory.isOpen()) {
 			if (inventoryHover && !InventorySystem::isExitObject(inventoryHover->object) &&
 					!InventorySystem::isStatusObject(inventoryHover->object) &&
@@ -466,7 +465,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 		}
 		hoverState.cursorSequence = 7;
 	} else if (_inventory.isOpen()) {
-		const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
+		const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
 		if (inventoryHover &&
 				!InventorySystem::isExitObject(inventoryHover->object)) {
 			inventoryTooltipText =
@@ -500,10 +499,10 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 			_inventory.resolveWeekdayLabel(_flow.getMenuTextConfig()));
 	}
 	if (inventorySelectionActive) {
-		_inventory.drawSelectedDragItem(*activeScreen, _flow._mousePos);
+		_inventory.drawSelectedDragItem(*activeScreen, _flow.getMousePos());
 	} else if (roomCarryActive && _roomItems.getCarriedRoomItemBitmap().isValid()) {
-		const int drawX = _flow._mousePos.x - (int)_roomItems.getCarriedRoomItemBitmap().width / 2;
-		const int drawY = _flow._mousePos.y - (int)_roomItems.getCarriedRoomItemBitmap().height / 2;
+		const int drawX = _flow.getMousePos().x - (int)_roomItems.getCarriedRoomItemBitmap().width / 2;
+		const int drawY = _flow.getMousePos().y - (int)_roomItems.getCarriedRoomItemBitmap().height / 2;
 		blitTransparentBitmap(*activeScreen, _roomItems.getCarriedRoomItemBitmap(), drawX, drawY);
 	}
 
@@ -688,7 +687,7 @@ InputResult Scene::runRoomFrames(const Common::String &currentRoomTarget) {
 	captureCurrentSaveState();
 
 	if (shouldRunStartupRoomProbe())
-		logStartupRoomProbe(_engine, _scene, currentRoomTarget, _flow._mousePos, _flow.getMenuTextConfig());
+		_flow.logStartupRoomProbe(_scene, currentRoomTarget);
 
 	while (!_engine.shouldQuit()) {
 		if (_flow.hasPendingMainMenuReturn())
@@ -1029,7 +1028,7 @@ Common::Error SceneRunner::runRoomLoop(Flow &flow, const Common::String &targetN
 		const RoomViewResources view = { art, promptFont, inspectFont, inventoryTooltipFont,
 			useNativePromptFont, useNativeInspectFont };
 		Scene session(_engine, flow, scene, entityManager, _inventory, _cheats, view,
-			flow._roomLoopDepth > 1);
+			flow.isNestedRoomLoop());
 		debugC(1, kDebugRoom, "Harvester: created room session room='%s'", scene.state.roomName.c_str());
 		session.initializePlayerState();
 


Commit: 102331f518468d242cf9bd562ebfbff645f6cc7c
    https://github.com/scummvm/scummvm/commit/102331f518468d242cf9bd562ebfbff645f6cc7c
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Remove remaining Flow friendships

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/flow.h


diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index 35e34459728..ca464b9535c 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -40,7 +40,6 @@ struct Event;
 namespace Harvester {
 
 class HarvesterEngine;
-class Interaction;
 struct IndexedBitmap;
 
 class Flow {
@@ -71,60 +70,58 @@ public:
 	bool queueDebugInteraction(const InteractionResult &interaction);
 	bool requestDebugRoomChange(const Common::String &roomName);
 
-private:
-	friend class MenuSystem;
-	friend class Interaction;
-	friend class SceneRunner;
-	friend class Input;
-	friend class Scene;
-
-	bool loadQuickTips();
-	bool loadMenuItems();
-	Common::Error runQuickTips();
-	Common::Error runDemoIntroduction();
 	Common::Error runDemoEnding();
-	Common::Error runMainMenu();
 	Common::Error runRoomMenu(const IndexedBitmap &backdrop, const byte *palette,
 		float paletteBrightness, bool canSaveGame);
 	Common::Error runRoomNpcDialogue(const IndexedBitmap &backdrop, const byte *palette,
 		float paletteBrightness, const ScriptExecutionContext &executionContext,
 		const NpcRecord &npc, const Common::String &usedItemName);
-	Common::Error runTownMapSelector(const Common::String &mapEntryName, Common::String &destinationEntranceName);
 	Common::Error runRoomLoop(const Common::String &entranceName);
 	Common::Error resolveRoomTransitionTarget(const Common::String &targetName, Common::String &resolvedTargetName);
-	bool ensureCursorEntity();
+
 	bool populateRoomSceneEntities(RoomSetupState &state,
 		const Common::Array<ObjectRecord> &drawableObjects,
 		const Common::Array<AnimRecord> &drawableAnimations);
 	Common::Error beginRoomSetupTransition();
 	Common::Error waitForRoomSetupTransitionHold();
 	Common::Error fadeInRoomScene(const byte *palette, float targetBrightness);
-	Common::Error fadePalette(const byte *palette, float fromBrightness, float toBrightness);
-	bool waitForPaletteFadeTick(uint32 startTick, Common::Error &result);
-	bool pumpTransitionEvents(Common::Error &result);
+	void resetCursorAnimationSequence();
+	bool tickRuntimeEntities();
+
 	bool takeQueuedDialogueInteraction(InteractionResult &interaction);
 	bool takeQueuedDebugInteraction(InteractionResult &interaction);
+
 	void prepareForNewGame();
 	void requestNewGameRestart();
-	bool hasPendingNewGameRestart() const;
 	bool takePendingNewGameRestart();
-	void clearPendingNewGameRestart();
-	bool hasPendingGameOverReturn() const;
 	bool takePendingGameOverReturn();
-	void clearPendingGameOverReturn();
 	void requestMainMenuReturn();
 	bool hasPendingMainMenuReturn() const;
 	bool takePendingMainMenuReturn();
-	void clearPendingMainMenuReturn();
 	void requestCloseupParentRestart();
-	bool hasPendingCloseupParentRestart() const;
 	bool takePendingCloseupParentRestart();
-	void clearPendingCloseupParentRestart();
 	bool hasPendingDebugRoomChange() const { return !_pendingDebugRoomName.empty(); }
-	Common::String takePendingDebugRoomChange();
 	void resetRoomNpcDialogueState();
-	void resetCursorAnimationSequence();
-	bool tickRuntimeEntities();
+
+private:
+	bool loadQuickTips();
+	bool loadMenuItems();
+	Common::Error runQuickTips();
+	Common::Error runDemoIntroduction();
+	Common::Error runMainMenu();
+	Common::Error runTownMapSelector(const Common::String &mapEntryName, Common::String &destinationEntranceName);
+	bool ensureCursorEntity();
+	Common::Error fadePalette(const byte *palette, float fromBrightness, float toBrightness);
+	bool waitForPaletteFadeTick(uint32 startTick, Common::Error &result);
+	bool pumpTransitionEvents(Common::Error &result);
+	bool hasPendingNewGameRestart() const;
+	void clearPendingNewGameRestart();
+	bool hasPendingGameOverReturn() const;
+	void clearPendingGameOverReturn();
+	void clearPendingMainMenuReturn();
+	bool hasPendingCloseupParentRestart() const;
+	void clearPendingCloseupParentRestart();
+	Common::String takePendingDebugRoomChange();
 
 	HarvesterEngine &_engine;
 	Common::Array<Common::String> _quickTips;


Commit: bad29b7c895a2ad71eb127b42ce42a3d0daf41a5
    https://github.com/scummvm/scummvm/commit/bad29b7c895a2ad71eb127b42ce42a3d0daf41a5
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Share room object entity construction

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/scene_entities.cpp
    engines/harvester/scene_entities.h


diff --git a/engines/harvester/scene_entities.cpp b/engines/harvester/scene_entities.cpp
index fba500d6c03..205a1b26472 100644
--- a/engines/harvester/scene_entities.cpp
+++ b/engines/harvester/scene_entities.cpp
@@ -30,41 +30,6 @@ namespace Harvester {
 
 static const char *const kInventoryOwnerName = "INVENTORY";
 
-static bool isBackgroundSceneObject(const RoomSetupState &state,
-		const ObjectRecord &object, const Entity &entity) {
-	if (!state.backgroundObjectName.empty() &&
-			object.objectName.equalsIgnoreCase(state.backgroundObjectName)) {
-		return true;
-	}
-
-	return object.initialX == 0 && object.initialY == 0 &&
-		entity.getBoundsWidth() == 640 && entity.getBoundsHeight() == 480;
-}
-
-static bool isInteractiveSceneHotspot(const ObjectRecord &object, Script *script) {
-	if (object.operatable || !object.actionTag.empty())
-		return true;
-	if (!script)
-		return false;
-	if (script->isPickupObject(object))
-		return true;
-
-	ResolvedText inspectText;
-	return script->resolveObjectInspectText(object, inspectText);
-}
-
-static int resolveSceneObjectClass(const RoomSetupState &state,
-		const ObjectRecord &object, const Entity *entity, Script *script) {
-	if (entity)
-		return isBackgroundSceneObject(state, object, *entity)
-			? kEntityClassBackground
-			: kEntityClassObject;
-
-	return isInteractiveSceneHotspot(object, script)
-		? kEntityClassRectHotspot
-		: kEntityClassDisabledHotspot;
-}
-
 static bool sameRect(const Common::Rect &lhs, const Common::Rect &rhs) {
 	return lhs.left == rhs.left && lhs.top == rhs.top &&
 		lhs.right == rhs.right && lhs.bottom == rhs.bottom;
@@ -540,12 +505,8 @@ EntityClass SceneEntities::resolveSceneObjectClassLocal(const ObjectRecord &obje
 		: kEntityClassDisabledHotspot;
 }
 
-void SceneEntities::spawnSceneObjectEntityFromRecord(const ObjectRecord &object) {
-	if (!_entityManager)
-		return;
-
-	const Common::String spritePath = resolveSceneObjectSpritePathLocal(object);
-	const Common::Rect hotspotBounds = getSceneObjectBounds(object);
+Entity *SceneEntities::createSceneObjectEntity(const ObjectRecord &object,
+		const Common::String &spritePath, const Common::Rect &hotspotBounds) {
 	Entity *entity = nullptr;
 	if (!spritePath.empty() && spritePath.hasSuffixIgnoreCase(".BM")) {
 		entity = _entityManager->spawnSceneBitmapEntity(object.objectName, spritePath,
@@ -555,12 +516,25 @@ void SceneEntities::spawnSceneObjectEntityFromRecord(const ObjectRecord &object)
 			(float)object.currentZ);
 	}
 	if (!entity)
-		return;
+		return nullptr;
 
 	entity->setClassId(resolveSceneObjectClassLocal(
 		object, entity->hasFrames() ? entity : nullptr));
 	entity->setAnchorMode(kEntityAnchorTopLeft);
 	entity->setZExtent((float)object.zExtent);
+	return entity;
+}
+
+void SceneEntities::spawnSceneObjectEntityFromRecord(const ObjectRecord &object) {
+	if (!_entityManager)
+		return;
+
+	const Common::String spritePath = resolveSceneObjectSpritePathLocal(object);
+	const Common::Rect hotspotBounds = getSceneObjectBounds(object);
+	Entity *entity = createSceneObjectEntity(object, spritePath, hotspotBounds);
+	if (!entity)
+		return;
+
 	debugC(3, kDebugRoom, "Harvester: scene object spawned object='%s' sprite='%s' class=0x%x",
 		object.objectName.c_str(), spritePath.c_str(), entity->getClassId());
 }
@@ -727,16 +701,9 @@ void SceneEntities::populateRoomObjects(const Common::Array<ObjectRecord> &drawa
 		return;
 
 	for (const ObjectRecord &object : drawableObjects) {
-		Entity *entity = nullptr;
 		const Common::String spritePath = resolveSceneObjectSpritePathLocal(object);
 		const Common::Rect hotspotBounds = getRoomObjectHotspotBounds(object);
-		if (!spritePath.empty() && spritePath.hasSuffixIgnoreCase(".BM")) {
-			entity = _entityManager->spawnSceneBitmapEntity(object.objectName, spritePath,
-				Common::Point(object.currentX, object.currentY), (float)object.currentZ);
-		} else {
-			if (!hotspotBounds.isEmpty())
-				entity = _entityManager->spawnSceneHotspotEntity(object.objectName, hotspotBounds, (float)object.currentZ);
-		}
+		Entity *entity = createSceneObjectEntity(object, spritePath, hotspotBounds);
 
 		if (!entity) {
 			debugC(1, kDebugRoom,
@@ -751,10 +718,6 @@ void SceneEntities::populateRoomObjects(const Common::Array<ObjectRecord> &drawa
 			continue;
 		}
 
-		entity->setClassId(resolveSceneObjectClass(
-			_state, object, entity->hasFrames() ? entity : nullptr, _script));
-		entity->setAnchorMode(kEntityAnchorTopLeft);
-		entity->setZExtent((float)object.zExtent);
 		const Common::Rect entityRect = entity->getScreenRect();
 		debugC(1, kDebugRoom,
 			"Harvester: scene entity spawned room='%s' object='%s' type='%s' class=0x%x pos=(%d,%d,z=%.2f) rect=(%d,%d)-(%d,%d) sprite='%s' action='%s' operatable=%d ident='%s'",
diff --git a/engines/harvester/scene_entities.h b/engines/harvester/scene_entities.h
index 7265a47da3a..77cc298c1f9 100644
--- a/engines/harvester/scene_entities.h
+++ b/engines/harvester/scene_entities.h
@@ -56,6 +56,8 @@ public:
 
 private:
 	bool isTimerMaterializedInCurrentRoom(const Common::String &timerName) const;
+	Entity *createSceneObjectEntity(const ObjectRecord &object,
+		const Common::String &spritePath, const Common::Rect &hotspotBounds);
 	bool isInteractiveSceneHotspot(const ObjectRecord &object) const;
 	EntityClass resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const;
 	static Common::Rect getSceneRegionBounds(const RegionRecord &region);


Commit: 41fbd50e02082cc8fd1aca6c977b8dabbb5d066c
    https://github.com/scummvm/scummvm/commit/41fbd50e02082cc8fd1aca6c977b8dabbb5d066c
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Share NPC animation configuration

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/scene_entities.cpp
    engines/harvester/scene_entities.h


diff --git a/engines/harvester/scene_entities.cpp b/engines/harvester/scene_entities.cpp
index 205a1b26472..c6c5490c5f7 100644
--- a/engines/harvester/scene_entities.cpp
+++ b/engines/harvester/scene_entities.cpp
@@ -593,6 +593,19 @@ bool SceneEntities::applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc)
 	return true;
 }
 
+void SceneEntities::configureRoomNpcAnimation(Entity &entity, const NpcRecord &npc, bool preservedCorpse) {
+	if (preservedCorpse) {
+		const int corpseFrame = MIN(entity.getLastFrame(), npc.runtimeState);
+		entity.setAnimationFrameRange(corpseFrame, corpseFrame, false);
+		entity.setAnimationRate(0);
+		entity.setCurrentFrame(corpseFrame);
+		entity.setAnimationEnabled(false);
+	} else {
+		// Native spawn_npc_entity_from_record seeds the passive room-NPC loop as frames 0..0x3b.
+		entity.setAnimationFrameRange(0, MIN(entity.getLastFrame(), kRoomNpcAmbientLastFrame), true);
+	}
+}
+
 Entity *SceneEntities::spawnSceneNpcEntityFromRecord(const NpcRecord &npc) {
 	const bool shouldSpawnNpc = shouldSpawnRoomNpcEntity(npc);
 	const bool preservedCorpse = !npc.active &&
@@ -615,16 +628,9 @@ Entity *SceneEntities::spawnSceneNpcEntityFromRecord(const NpcRecord &npc) {
 		removeSceneEntityByName(npc.npcName);
 		return nullptr;
 	}
-	if (preservedCorpse) {
-		const int corpseFrame = MIN(entity->getLastFrame(), npc.runtimeState);
-		entity->setAnimationFrameRange(corpseFrame, corpseFrame, false);
-		entity->setAnimationRate(0);
-		entity->setCurrentFrame(corpseFrame);
-		entity->setAnimationEnabled(false);
-	} else {
-		entity->setAnimationFrameRange(0, MIN(entity->getLastFrame(), kRoomNpcAmbientLastFrame), true);
+	configureRoomNpcAnimation(*entity, npc, preservedCorpse);
+	if (!preservedCorpse)
 		entity->setAnimationRate(npc.frameDelay > 0 ? npc.frameDelay : 0);
-	}
 	_entityManager->reinsertSceneEntity(entity);
 	return entity;
 }
@@ -754,18 +760,9 @@ void SceneEntities::populateRoomNpcs() {
 			debug(1, "Harvester: unable to apply room npc placement for '%s'",
 				npc.npcName.c_str());
 		}
-		if (isCorpse) {
-			const int corpseFrame = MIN(entity->getLastFrame(), npc.runtimeState);
-			entity->setAnimationFrameRange(corpseFrame, corpseFrame, false);
-			entity->setAnimationRate(0);
-			entity->setCurrentFrame(corpseFrame);
-			entity->setAnimationEnabled(false);
-		} else {
-			// Native spawn_npc_entity_from_record seeds the passive room-NPC loop as frames 0..0x3b.
-			entity->setAnimationFrameRange(0, MIN(entity->getLastFrame(), kRoomNpcAmbientLastFrame), true);
-			if (npc.frameDelay > 0)
-				entity->setAnimationRate(npc.frameDelay);
-		}
+		configureRoomNpcAnimation(*entity, npc, isCorpse);
+		if (!isCorpse && npc.frameDelay > 0)
+			entity->setAnimationRate(npc.frameDelay);
 		_entityManager->reinsertSceneEntity(entity);
 		debugC(1, kDebugRoom,
 			"Harvester: scene npc spawned room='%s' npc='%s' class=0x%x pos=(%d,%d,z=%.2f) frame_delay=%d model='%s' active=%d visible=%d",
diff --git a/engines/harvester/scene_entities.h b/engines/harvester/scene_entities.h
index 77cc298c1f9..ee026d1eb37 100644
--- a/engines/harvester/scene_entities.h
+++ b/engines/harvester/scene_entities.h
@@ -62,6 +62,7 @@ private:
 	EntityClass resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const;
 	static Common::Rect getSceneRegionBounds(const RegionRecord &region);
 	static bool applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc);
+	static void configureRoomNpcAnimation(Entity &entity, const NpcRecord &npc, bool preservedCorpse);
 
 	RoomSetupState &_state;
 	Script *_script;


Commit: 0c2252ad1520011e2dd3ddc55b1262f8552fce0b
    https://github.com/scummvm/scummvm/commit/0c2252ad1520011e2dd3ddc55b1262f8552fce0b
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Share monster entity construction

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/scene_entities.cpp
    engines/harvester/scene_entities.h


diff --git a/engines/harvester/scene_entities.cpp b/engines/harvester/scene_entities.cpp
index c6c5490c5f7..ea36d0bc160 100644
--- a/engines/harvester/scene_entities.cpp
+++ b/engines/harvester/scene_entities.cpp
@@ -635,10 +635,7 @@ Entity *SceneEntities::spawnSceneNpcEntityFromRecord(const NpcRecord &npc) {
 	return entity;
 }
 
-Entity *SceneEntities::spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster) {
-	if (!_entityManager || !shouldSpawnRoomMonsterEntity(monster))
-		return nullptr;
-
+Entity *SceneEntities::createSceneMonsterEntity(const MonsterRecord &monster) {
 	Entity *entity = _entityManager->spawnSceneActorEntity(
 		monster.monsterName, monster.modelPath,
 		Common::Point(monster.posX, monster.posY), (float)monster.posZ,
@@ -651,6 +648,17 @@ Entity *SceneEntities::spawnSceneMonsterEntityFromRecord(const RoomSetupState &r
 	entity->setHitTestMode(kEntityHitTestNone);
 	entity->setVisible(monster.visible);
 	Monster::applyAnimation(*entity, monster);
+	return entity;
+}
+
+Entity *SceneEntities::spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster) {
+	if (!_entityManager || !shouldSpawnRoomMonsterEntity(monster))
+		return nullptr;
+
+	Entity *entity = createSceneMonsterEntity(monster);
+	if (!entity)
+		return nullptr;
+
 	if (!applyRoomActorPlacement(roomState, *entity, monster.posX, monster.posY, (float)monster.posZ)) {
 		removeSceneEntityByName(monster.monsterName);
 		return nullptr;
@@ -777,21 +785,13 @@ void SceneEntities::populateRoomMonsters() {
 		return;
 
 	for (const MonsterRecord &monster : _state.roomMonsters) {
-		Entity *entity = _entityManager->spawnSceneActorEntity(monster.monsterName,
-			monster.modelPath, Common::Point(monster.posX, monster.posY), (float)monster.posZ,
-			Monster::resolveFacingFrame(monster.facing));
+		Entity *entity = createSceneMonsterEntity(monster);
 		if (!entity) {
 			debug(1, "Harvester: unable to spawn room monster entity '%s' from '%s'",
 				monster.monsterName.c_str(), monster.modelPath.c_str());
 			continue;
 		}
 
-		entity->setClassId(kEntityClassMonster);
-		entity->setZExtent(kNativeNpcMonsterZExtent);
-		entity->setHitTestMode(kEntityHitTestNone);
-		entity->setVisible(monster.visible);
-		// Native restores a saved monster corpse frame before set_entity_screen_position().
-		Monster::applyAnimation(*entity, monster);
 		if (!applyRoomActorPlacement(_state, *entity, monster.posX, monster.posY, (float)monster.posZ)) {
 			debug(1, "Harvester: unable to apply room monster placement for '%s'",
 				monster.monsterName.c_str());
diff --git a/engines/harvester/scene_entities.h b/engines/harvester/scene_entities.h
index ee026d1eb37..480b3740af5 100644
--- a/engines/harvester/scene_entities.h
+++ b/engines/harvester/scene_entities.h
@@ -63,6 +63,7 @@ private:
 	static Common::Rect getSceneRegionBounds(const RegionRecord &region);
 	static bool applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc);
 	static void configureRoomNpcAnimation(Entity &entity, const NpcRecord &npc, bool preservedCorpse);
+	Entity *createSceneMonsterEntity(const MonsterRecord &monster);
 
 	RoomSetupState &_state;
 	Script *_script;


Commit: db94aa0a4ed73dbda1d3a1ac99699b2d9e8b5b10
    https://github.com/scummvm/scummvm/commit/db94aa0a4ed73dbda1d3a1ac99699b2d9e8b5b10
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Share region entity construction

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/scene_entities.cpp
    engines/harvester/scene_entities.h


diff --git a/engines/harvester/scene_entities.cpp b/engines/harvester/scene_entities.cpp
index ea36d0bc160..fef3d4a21f6 100644
--- a/engines/harvester/scene_entities.cpp
+++ b/engines/harvester/scene_entities.cpp
@@ -545,18 +545,12 @@ Common::Rect SceneEntities::getSceneRegionBounds(const RegionRecord &region) {
 	return Common::Rect();
 }
 
-void SceneEntities::spawnSceneRegionEntityFromRecord(const RegionRecord &region) {
-	if (!_entityManager)
-		return;
-
-	const Common::Rect bounds = getSceneRegionBounds(region);
-	if (bounds.isEmpty())
-		return;
-
+Entity *SceneEntities::createSceneRegionEntity(const RegionRecord &region,
+		const Common::Rect &bounds, const Common::String &diagnosticRoomName) {
 	Entity *entity = _entityManager->spawnSceneHotspotEntity(
 		region.regionName, bounds, (float)region.minZ);
 	if (!entity)
-		return;
+		return nullptr;
 
 	entity->setClassId(kEntityClassRectHotspot19);
 	entity->setAnchorMode(kEntityAnchorTopLeft);
@@ -565,9 +559,21 @@ void SceneEntities::spawnSceneRegionEntityFromRecord(const RegionRecord &region)
 		entity->setHitTestMode(kEntityHitTestNone);
 	debugC(1, kDebugRoom,
 		"Harvester: scene region spawned room='%s' region='%s' class=0x%x bounds=(%d,%d)-(%d,%d) z=[%d,%d] facing=%d cursor=%d action='%s'",
-		region.roomName.c_str(), region.regionName.c_str(), entity->getClassId(),
+		diagnosticRoomName.c_str(), region.regionName.c_str(), entity->getClassId(),
 		bounds.left, bounds.top, bounds.right, bounds.bottom,
 		region.minZ, region.maxZ, region.desiredFacing, region.cursorEnabled, region.actionTag.c_str());
+	return entity;
+}
+
+void SceneEntities::spawnSceneRegionEntityFromRecord(const RegionRecord &region) {
+	if (!_entityManager)
+		return;
+
+	const Common::Rect bounds = getSceneRegionBounds(region);
+	if (bounds.isEmpty())
+		return;
+
+	(void)createSceneRegionEntity(region, bounds, region.roomName);
 }
 
 bool SceneEntities::shouldSpawnRoomNpcEntity(const NpcRecord &npc) {
@@ -686,9 +692,7 @@ void SceneEntities::populateRoomRegions() {
 		if (regionBounds.isEmpty())
 			continue;
 
-		Entity *entity = _entityManager->spawnSceneHotspotEntity(
-			region.regionName, regionBounds, (float)region.minZ);
-		if (!entity) {
+		if (!createSceneRegionEntity(region, regionBounds, _state.roomName)) {
 			debugC(1, kDebugRoom,
 				"Harvester: scene region skipped room='%s' region='%s' bounds=(%d,%d)-(%d,%d) z=[%d,%d]",
 				_state.roomName.c_str(), region.regionName.c_str(),
@@ -696,17 +700,6 @@ void SceneEntities::populateRoomRegions() {
 				region.minZ, region.maxZ);
 			continue;
 		}
-
-		entity->setClassId(kEntityClassRectHotspot19);
-		entity->setAnchorMode(kEntityAnchorTopLeft);
-		entity->setZExtent((float)MAX(0, region.maxZ - region.minZ));
-		if (!region.cursorEnabled)
-			entity->setHitTestMode(kEntityHitTestNone);
-		debugC(1, kDebugRoom,
-			"Harvester: scene region spawned room='%s' region='%s' class=0x%x bounds=(%d,%d)-(%d,%d) z=[%d,%d] facing=%d cursor=%d action='%s'",
-			_state.roomName.c_str(), region.regionName.c_str(), entity->getClassId(),
-			regionBounds.left, regionBounds.top, regionBounds.right, regionBounds.bottom,
-			region.minZ, region.maxZ, region.desiredFacing, region.cursorEnabled, region.actionTag.c_str());
 	}
 }
 
diff --git a/engines/harvester/scene_entities.h b/engines/harvester/scene_entities.h
index 480b3740af5..9bcc84f5679 100644
--- a/engines/harvester/scene_entities.h
+++ b/engines/harvester/scene_entities.h
@@ -61,6 +61,8 @@ private:
 	bool isInteractiveSceneHotspot(const ObjectRecord &object) const;
 	EntityClass resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const;
 	static Common::Rect getSceneRegionBounds(const RegionRecord &region);
+	Entity *createSceneRegionEntity(const RegionRecord &region,
+		const Common::Rect &bounds, const Common::String &diagnosticRoomName);
 	static bool applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc);
 	static void configureRoomNpcAnimation(Entity &entity, const NpcRecord &npc, bool preservedCorpse);
 	Entity *createSceneMonsterEntity(const MonsterRecord &monster);


Commit: 88ff885adbbc01c82311bccf9b9dc06af4d6c1a3
    https://github.com/scummvm/scummvm/commit/88ff885adbbc01c82311bccf9b9dc06af4d6c1a3
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Move inspect text rendering into RoomPresentation

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/flow.cpp
    engines/harvester/input.cpp
    engines/harvester/room_presentation.cpp
    engines/harvester/room_presentation.h
    engines/harvester/room_support.h
    engines/harvester/scene.cpp


diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index e88d6ed5eee..e76fdfcebe8 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -74,10 +74,6 @@ static const char *const kTownMapBitmapPaths[] = {
 };
 
 static const int kCursorSequence7 = 7;
-static const int kIdentTextboxX = 177;
-static const int kIdentTextboxY = 85;
-static const int kIdentTextboxTextInsetX = 10;
-static const int kIdentTextboxTextInsetY = 5;
 static const int kNativeIdentTextLineSpacing = 3;
 static const char *const kPlayerActorEntityName = "PLAYER";
 // ramp_palette_brightness (0x23a30) waits four DOS centiseconds per step.
@@ -102,8 +98,6 @@ static const uint32 kTransitionWaitFrameMinMs = 120;
 static const int kTownMapEdgeThreshold = 9;
 static const int kTownMapCursorHitExtent = 5;
 
-static const byte kIdentTextColor = 0xd3;
-static const byte kShadowColor = 0;
 static const byte kTownMapLabelColor = 0x28;
 
 static const int kCursorSequenceWalk = 0;
@@ -272,13 +266,6 @@ void logScenePaletteSummary(const char *label, const SceneState &scene, float br
 		label, scene.state.roomName.c_str(), scene.state.palettePath.c_str(), (double)brightness);
 }
 
-static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
-	if (!bitmap.isValid())
-		return;
-
-	screen.copyRectToSurface(bitmap.pixels.data(), bitmap.width, x, y, bitmap.width, bitmap.height);
-}
-
 bool captureScreenBackdrop(const Graphics::Screen &screen, IndexedBitmap &bitmap) {
 	if (screen.w <= 0 || screen.h <= 0 || screen.format.bytesPerPixel != 1)
 		return false;
@@ -292,35 +279,6 @@ bool captureScreenBackdrop(const Graphics::Screen &screen, IndexedBitmap &bitmap
 	return true;
 }
 
-static void drawShadowedString(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
-		int x, int y, int width, byte color, Graphics::TextAlign align = Graphics::kTextAlignLeft) {
-	font.drawString(&screen, text, x + 1, y + 1, width, kShadowColor, align);
-	font.drawString(&screen, text, x, y, width, color, align);
-}
-
-static void drawWrappedShadowedText(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
-		int x, int y, int width, byte color) {
-	Common::Array<Common::String> lines;
-	font.wordWrapText(text, width, lines);
-
-	const int lineHeight = font.getFontHeight() + 2;
-	for (uint i = 0; i < lines.size(); ++i)
-		drawShadowedString(screen, font, lines[i], x, y + i * lineHeight, width, color);
-}
-
-static void drawWrappedText(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
-		int x, int y, int width, byte color, int lineSpacing, bool useCftCharacterWrapping = false) {
-	Common::Array<Common::String> lines;
-	if (useCftCharacterWrapping)
-		wrapCftTextByCharacterCount(font, text, width, lines);
-	else
-		font.wordWrapText(text, width, lines);
-
-	const int lineHeight = font.getFontHeight() + lineSpacing;
-	for (uint i = 0; i < lines.size(); ++i)
-		font.drawString(&screen, lines[i], x, y + i * lineHeight, width, color);
-}
-
 static void setScaledPalette(Graphics::Screen &screen, const byte *palette, float brightness) {
 	byte scaledPalette[256 * 3];
 	const float gammaBrightness = g_engine ? g_engine->getGammaBrightnessScale() : 1.0f;
@@ -614,44 +572,6 @@ static const NpcRecord *findRoomNpcAtPoint(HarvesterEngine &engine,
 	return npc;
 }
 
-const IndexedBitmap *resolveInspectTextboxBitmap(const Art &art, const ResolvedText &text) {
-	if (text.boxName.equalsIgnoreCase("BOX1"))
-		return art.getTextboxBitmap(0);
-	if (text.boxName.equalsIgnoreCase("BOX2"))
-		return art.getTextboxBitmap(1);
-	if (text.boxName.equalsIgnoreCase("BOX3"))
-		return art.getTextboxBitmap(2);
-	if (text.boxName.equalsIgnoreCase("BOX4"))
-		return art.getTextboxBitmap(3);
-
-	return nullptr;
-}
-
-void drawRoomInspectText(Graphics::Screen &screen, const Art &art, const Graphics::Font &font,
-		const ResolvedText &inspectText, bool useNativeFont) {
-	const IndexedBitmap *textbox = resolveInspectTextboxBitmap(art, inspectText);
-	if (!textbox || !textbox->isValid())
-		return;
-
-	blitBitmap(screen, *textbox, kIdentTextboxX, kIdentTextboxY);
-	if (useNativeFont) {
-		drawWrappedText(screen, font, inspectText.value,
-			kIdentTextboxX + kIdentTextboxTextInsetX,
-			kIdentTextboxY + kIdentTextboxTextInsetY,
-			MAX<int>(0, (int)textbox->width - 2),
-			0,
-			kNativeIdentTextLineSpacing,
-			true);
-		return;
-	}
-
-	drawWrappedShadowedText(screen, font, inspectText.value,
-		kIdentTextboxX + kIdentTextboxTextInsetX,
-		kIdentTextboxY + kIdentTextboxTextInsetY,
-		MAX<int>(0, (int)textbox->width - (kIdentTextboxTextInsetX + 2)),
-		kIdentTextColor);
-}
-
 static bool suppressesInitialObjectInspectGate(const ObjectRecord &object) {
 	// Native HARVEST.SCR uses ident key X as a no-text sentinel.
 	return object.identTextKey.equalsIgnoreCase("X");
@@ -1278,7 +1198,7 @@ Common::Error Flow::runDemoEnding() {
 
 		setScaledPalette(*screen, palette, kPaletteBrightnessBlack);
 		screen->fillRect(Common::Rect(0, 0, screen->w, screen->h), 0);
-		blitBitmap(*screen, bitmap, 0, 0);
+		RoomPresentation::blitBitmap(*screen, bitmap, 0, 0);
 		screen->makeAllDirty();
 		screen->update();
 
@@ -1347,9 +1267,9 @@ Common::Error Flow::runDemoIntroduction() {
 		entityManager->hideCursor();
 	setScaledPalette(*screen, palette, kPaletteBrightnessBlack);
 	screen->fillRect(Common::Rect(0, 0, screen->w, screen->h), 0);
-	blitBitmap(*screen, bitmap, 0, 0);
+	RoomPresentation::blitBitmap(*screen, bitmap, 0, 0);
 	blitTransparentBitmap(*screen, art->getLogoBitmap(), 160, 0);
-	drawWrappedText(*screen, font, kDemoIntroductionText, 90, 200, 420, 0xce,
+	RoomPresentation::drawWrappedText(*screen, font, kDemoIntroductionText, 90, 200, 420, 0xce,
 		kNativeIdentTextLineSpacing);
 	screen->makeAllDirty();
 	screen->update();
@@ -1528,9 +1448,9 @@ Common::Error Flow::runTownMapSelector(const Common::String &mapEntryName,
 		if (needsRedraw) {
 			setScaledPalette(*screen, palette, paletteBrightness);
 			screen->fillRect(screen->getBounds(), 0);
-			blitBitmap(*screen, panels[(uint)currentPanel], 0, 0);
+			RoomPresentation::blitBitmap(*screen, panels[(uint)currentPanel], 0, 0);
 			if (hoveredLocation) {
-				drawShadowedString(*screen, *font, hoveredLocation->labelText,
+				RoomPresentation::drawShadowedString(*screen, *font, hoveredLocation->labelText,
 					hoveredLocation->labelX, hoveredLocation->labelY, screen->w, kTownMapLabelColor);
 			}
 			if (EntityManager *entityManager = _engine.getRuntimeEntities())
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index 6029618b424..408f298420d 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -546,7 +546,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 	const bool unlocksAfterInitialExamine =
 		unlocksRoomObjectInteractionAfterInitialExamine(*clickedObject, *_engine.getScript());
 	const bool canShowInspectText =
-		hasInspectText && resolveInspectTextboxBitmap(_art, resolvedInspectText);
+		hasInspectText && RoomPresentation::resolveInspectTextboxBitmap(_art, resolvedInspectText);
 	if (!clickedObject->identShown && unlocksAfterInitialExamine) {
 		clickedObject->identShown = true;
 		_engine.getScript()->markObjectIdentShown(*clickedObject);
diff --git a/engines/harvester/room_presentation.cpp b/engines/harvester/room_presentation.cpp
index 1807e9f44f0..936a77c136f 100644
--- a/engines/harvester/room_presentation.cpp
+++ b/engines/harvester/room_presentation.cpp
@@ -30,6 +30,7 @@
 #include "graphics/fontman.h"
 #include "graphics/framelimiter.h"
 #include "harvester/art.h"
+#include "harvester/cft_font.h"
 #include "harvester/detection.h"
 #include "harvester/entity.h"
 #include "harvester/flow.h"
@@ -43,6 +44,13 @@ namespace Harvester {
 
 static const float kNormalPaletteBrightness = 1.0f;
 static const float kDimPaletteBrightness = 0.6f;
+static const int kIdentTextboxX = 177;
+static const int kIdentTextboxY = 85;
+static const int kIdentTextboxTextInsetX = 10;
+static const int kIdentTextboxTextInsetY = 5;
+static const byte kIdentTextColor = 0xd3;
+static const byte kShadowColor = 0;
+static const int kNativeIdentTextLineSpacing = 3;
 static const char *const kPlayerActorEntityName = "PLAYER";
 static const int kRoomDebugLabelPaddingX = 3;
 static const int kRoomDebugLabelPaddingY = 2;
@@ -367,6 +375,80 @@ void RoomPresentation::drawRoomScene(HarvesterEngine &engine, Graphics::Screen &
 	drawTimerDebugOverlay(engine, screen, scene);
 }
 
+void RoomPresentation::blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
+	if (!bitmap.isValid())
+		return;
+
+	screen.copyRectToSurface(bitmap.pixels.data(), bitmap.width, x, y, bitmap.width, bitmap.height);
+}
+
+void RoomPresentation::drawShadowedString(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
+		int x, int y, int width, byte color, Graphics::TextAlign align) {
+	font.drawString(&screen, text, x + 1, y + 1, width, kShadowColor, align);
+	font.drawString(&screen, text, x, y, width, color, align);
+}
+
+static void drawWrappedShadowedText(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
+		int x, int y, int width, byte color) {
+	Common::Array<Common::String> lines;
+	font.wordWrapText(text, width, lines);
+
+	const int lineHeight = font.getFontHeight() + 2;
+	for (uint i = 0; i < lines.size(); ++i)
+		RoomPresentation::drawShadowedString(screen, font, lines[i], x, y + i * lineHeight, width, color);
+}
+
+void RoomPresentation::drawWrappedText(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
+		int x, int y, int width, byte color, int lineSpacing, bool useCftCharacterWrapping) {
+	Common::Array<Common::String> lines;
+	if (useCftCharacterWrapping)
+		wrapCftTextByCharacterCount(font, text, width, lines);
+	else
+		font.wordWrapText(text, width, lines);
+
+	const int lineHeight = font.getFontHeight() + lineSpacing;
+	for (uint i = 0; i < lines.size(); ++i)
+		font.drawString(&screen, lines[i], x, y + i * lineHeight, width, color);
+}
+
+const IndexedBitmap *RoomPresentation::resolveInspectTextboxBitmap(const Art &art, const ResolvedText &text) {
+	if (text.boxName.equalsIgnoreCase("BOX1"))
+		return art.getTextboxBitmap(0);
+	if (text.boxName.equalsIgnoreCase("BOX2"))
+		return art.getTextboxBitmap(1);
+	if (text.boxName.equalsIgnoreCase("BOX3"))
+		return art.getTextboxBitmap(2);
+	if (text.boxName.equalsIgnoreCase("BOX4"))
+		return art.getTextboxBitmap(3);
+
+	return nullptr;
+}
+
+void RoomPresentation::drawRoomInspectText(Graphics::Screen &screen, const Art &art, const Graphics::Font &font,
+		const ResolvedText &inspectText, bool useNativeFont) {
+	const IndexedBitmap *textbox = resolveInspectTextboxBitmap(art, inspectText);
+	if (!textbox || !textbox->isValid())
+		return;
+
+	blitBitmap(screen, *textbox, kIdentTextboxX, kIdentTextboxY);
+	if (useNativeFont) {
+		drawWrappedText(screen, font, inspectText.value,
+			kIdentTextboxX + kIdentTextboxTextInsetX,
+			kIdentTextboxY + kIdentTextboxTextInsetY,
+			MAX<int>(0, (int)textbox->width - 2),
+			0,
+			kNativeIdentTextLineSpacing,
+			true);
+		return;
+	}
+
+	drawWrappedShadowedText(screen, font, inspectText.value,
+		kIdentTextboxX + kIdentTextboxTextInsetX,
+		kIdentTextboxY + kIdentTextboxTextInsetY,
+		MAX<int>(0, (int)textbox->width - (kIdentTextboxTextInsetX + 2)),
+		kIdentTextColor);
+}
+
 RoomPresentation::RoomPresentation(HarvesterEngine &engine, SceneState &scene,
 		const Art *art, const Graphics::Font *inspectFont, bool useNativeInspectFont)
 	: _engine(engine), _scene(scene), _art(art), _inspectFont(inspectFont),
diff --git a/engines/harvester/room_presentation.h b/engines/harvester/room_presentation.h
index 577da6b4fe3..008531dbe41 100644
--- a/engines/harvester/room_presentation.h
+++ b/engines/harvester/room_presentation.h
@@ -23,10 +23,10 @@
 #define HARVESTER_ROOM_PRESENTATION_H
 
 #include "common/error.h"
+#include "graphics/font.h"
 #include "harvester/script.h"
 
 namespace Graphics {
-class Font;
 class Screen;
 }
 
@@ -66,6 +66,15 @@ public:
 	static void drawRoomScene(HarvesterEngine &engine, Graphics::Screen &screen,
 		const SceneState &scene, float brightness);
 
+	static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y);
+	static void drawShadowedString(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
+		int x, int y, int width, byte color, Graphics::TextAlign align = Graphics::kTextAlignLeft);
+	static void drawWrappedText(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
+		int x, int y, int width, byte color, int lineSpacing, bool useCftCharacterWrapping = false);
+	static const IndexedBitmap *resolveInspectTextboxBitmap(const Art &art, const ResolvedText &text);
+	static void drawRoomInspectText(Graphics::Screen &screen, const Art &art, const Graphics::Font &font,
+		const ResolvedText &inspectText, bool useNativeFont);
+
 	bool captureDialogueBackdrop(IndexedBitmap &dialogueBackdrop);
 	Common::Error applyLightingCommand(StartupLightingCommand lightingCommand);
 	Common::Error runModalShowText(const ResolvedText &modalText, Flow &flow);
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index b8b110a4a0a..9bc604d253a 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -79,9 +79,6 @@ const RegionRecord *findSceneRegionByName(const Common::Array<RegionRecord> &reg
 	const Common::String &regionName);
 ObjectRecord *findSceneObjectByName(Common::Array<ObjectRecord> &objects,
 	const Common::String &objectName);
-const IndexedBitmap *resolveInspectTextboxBitmap(const Art &art, const ResolvedText &text);
-void drawRoomInspectText(Graphics::Screen &screen, const Art &art, const Graphics::Font &font,
-	const ResolvedText &inspectText, bool useNativeFont);
 bool unlocksRoomObjectInteractionAfterInitialExamine(const ObjectRecord &object,
 	Script &script);
 bool doesPlayerFacingMatchRegion(int playerFacing, const RegionRecord &region);
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index 9d59fd9fde7..b2de54442b2 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -507,12 +507,12 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 	}
 
 	if (_presentationState.showingInspectText) {
-		drawRoomInspectText(*activeScreen, *_view.art, *_view.inspectFont, _presentationState.inspectText, _view.useNativeInspectFont);
+		RoomPresentation::drawRoomInspectText(*activeScreen, *_view.art, *_view.inspectFont, _presentationState.inspectText, _view.useNativeInspectFont);
 		if (!_presentationState.currentInteractionPromptText.empty())
 			drawRoomPrompt(*activeScreen, *_view.promptFont,
 				_presentationState.currentInteractionPromptText, _view.useNativePromptFont);
 	} else if (!_presentationState.combatLoadoutStatusText.value.empty()) {
-		drawRoomInspectText(*activeScreen, *_view.art, *_view.inspectFont, _presentationState.combatLoadoutStatusText,
+		RoomPresentation::drawRoomInspectText(*activeScreen, *_view.art, *_view.inspectFont, _presentationState.combatLoadoutStatusText,
 			_view.useNativeInspectFont);
 	} else if (!inventoryTooltipText.empty()) {
 		drawInventoryTooltip(*activeScreen, *_view.inventoryTooltipFont, inventoryTooltipText);


Commit: 9f90244baac05e6b2d1617c39b3605a293b663ee
    https://github.com/scummvm/scummvm/commit/9f90244baac05e6b2d1617c39b3605a293b663ee
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:07-04:00

Commit Message:
HARVESTER: Move scene record lookups into SceneEntities

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/flow.cpp
    engines/harvester/input.cpp
    engines/harvester/interaction.cpp
    engines/harvester/room_support.h
    engines/harvester/scene.cpp
    engines/harvester/scene_entities.cpp
    engines/harvester/scene_entities.h


diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index e76fdfcebe8..a43ae5f9721 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -425,36 +425,6 @@ bool shouldRunStartupRoomProbe() {
 		ConfMan.getBool("harvester_debug_probe_startup_room");
 }
 
-const ObjectRecord *findSceneObjectByName(const Common::Array<ObjectRecord> &objects,
-		const Common::String &objectName) {
-	for (const ObjectRecord &object : objects) {
-		if (object.objectName.equalsIgnoreCase(objectName))
-			return &object;
-	}
-
-	return nullptr;
-}
-
-const RegionRecord *findSceneRegionByName(const Common::Array<RegionRecord> &regions,
-		const Common::String &regionName) {
-	for (const RegionRecord &region : regions) {
-		if (region.regionName.equalsIgnoreCase(regionName))
-			return ®ion;
-	}
-
-	return nullptr;
-}
-
-static const NpcRecord *findSceneNpcByName(const Common::Array<NpcRecord> &npcs,
-		const Common::String &npcName) {
-	for (const NpcRecord &npc : npcs) {
-		if (npc.npcName.equalsIgnoreCase(npcName))
-			return &npc;
-	}
-
-	return nullptr;
-}
-
 static bool canTalkToRoomNpc(HarvesterEngine &engine, const NpcRecord &npc,
 		const DialogueSystem *dialogue) {
 	Script *script = engine.getScript();
@@ -464,16 +434,6 @@ static bool canTalkToRoomNpc(HarvesterEngine &engine, const NpcRecord &npc,
 	return dialogue && dialogue->hasRoomNpcHandler(npc.npcName);
 }
 
-ObjectRecord *findSceneObjectByName(Common::Array<ObjectRecord> &objects,
-		const Common::String &objectName) {
-	for (ObjectRecord &object : objects) {
-		if (object.objectName.equalsIgnoreCase(objectName))
-			return &object;
-	}
-
-	return nullptr;
-}
-
 static const ObjectRecord *findRoomObjectAtPoint(HarvesterEngine &engine,
 		const Common::Array<ObjectRecord> &sceneObjects, const Common::Point &point) {
 	EntityManager *entityManager = engine.getRuntimeEntities();
@@ -539,7 +499,7 @@ static const RegionRecord *findRoomRegionAtPoint(HarvesterEngine &engine,
 	if (!topEntity)
 		return nullptr;
 
-	return findSceneRegionByName(sceneRegions, topEntity->getName());
+	return SceneEntities::findSceneRegionByName(sceneRegions, topEntity->getName());
 }
 
 static const Entity *findRoomPlayerAtPoint(HarvesterEngine &engine, const Common::Point &point) {
@@ -565,7 +525,7 @@ static const NpcRecord *findRoomNpcAtPoint(HarvesterEngine &engine,
 	if (!entity || entity->getClassId() != kEntityClassNpc)
 		return nullptr;
 
-	const NpcRecord *npc = findSceneNpcByName(sceneNpcs, entity->getName());
+	const NpcRecord *npc = SceneEntities::findSceneNpcByName(sceneNpcs, entity->getName());
 	if (!npc || !canTalkToRoomNpc(engine, *npc, dialogue))
 		return nullptr;
 
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index 408f298420d..dee80e80534 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -34,6 +34,7 @@
 #include "harvester/room_presentation.h"
 #include "harvester/scene.h"
 #include "harvester/room_support.h"
+#include "harvester/scene_entities.h"
 
 namespace Harvester {
 
@@ -400,7 +401,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 				}
 			}
 		}
-		clickHoverState.object = findSceneObjectByName(_scene.sceneObjects, itemTarget->getName());
+		clickHoverState.object = SceneEntities::findSceneObjectByName(_scene.sceneObjects, itemTarget->getName());
 	}
 	debugC(1, kDebugRoom,
 		"Harvester: room click room='%s' mouse=(%d,%d) object='%s' npc='%s' region='%s' cursor_sequence=%d prompt='%s'",
@@ -434,7 +435,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 		}
 
 		ObjectRecord *roomTarget = clickHoverState.object
-			? findSceneObjectByName(_scene.sceneObjects, clickHoverState.object->objectName)
+			? SceneEntities::findSceneObjectByName(_scene.sceneObjects, clickHoverState.object->objectName)
 			: nullptr;
 		if (roomTarget) {
 			Common::Error interactionError =
@@ -502,7 +503,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
 		return InputResult();
 	}
 	ObjectRecord *clickedObject = hoverState.object
-		? findSceneObjectByName(_scene.sceneObjects, hoverState.object->objectName)
+		? SceneEntities::findSceneObjectByName(_scene.sceneObjects, hoverState.object->objectName)
 		: nullptr;
 	if (!clickedObject) {
 		_session.clearPendingRegionInteraction();
diff --git a/engines/harvester/interaction.cpp b/engines/harvester/interaction.cpp
index 09bd3be59c9..a5f33947652 100644
--- a/engines/harvester/interaction.cpp
+++ b/engines/harvester/interaction.cpp
@@ -111,7 +111,7 @@ void ItemInteractions::clearCarriedRoomItem() {
 }
 
 void ItemInteractions::hideSceneObject(const Common::String &objectName, const Common::String *ownerOrRoom) {
-	ObjectRecord *sceneObject = findSceneObjectByName(_scene.sceneObjects, objectName);
+	ObjectRecord *sceneObject = SceneEntities::findSceneObjectByName(_scene.sceneObjects, objectName);
 	if (!sceneObject)
 		return;
 
@@ -426,7 +426,7 @@ Common::Error Interaction::tryActivatePendingRegion(InventorySystem &inventory,
 	if (_pendingRegionName.empty() || !playerState.entity)
 		return Common::kNoError;
 
-	const RegionRecord *region = findSceneRegionByName(_scene.sceneRegions, _pendingRegionName);
+	const RegionRecord *region = SceneEntities::findSceneRegionByName(_scene.sceneRegions, _pendingRegionName);
 	if (!region || !region->startEnabled) {
 		_pendingRegionName.clear();
 		return Common::kNoError;
@@ -464,7 +464,7 @@ Common::Error Interaction::tryActivatePassiveRegion(InventorySystem &inventory,
 }
 
 int Interaction::getPendingRegionFacing() const {
-	const RegionRecord *pendingRegion = findSceneRegionByName(_scene.sceneRegions, _pendingRegionName);
+	const RegionRecord *pendingRegion = SceneEntities::findSceneRegionByName(_scene.sceneRegions, _pendingRegionName);
 	return pendingRegion && pendingRegion->startEnabled ? pendingRegion->desiredFacing : -1;
 }
 
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index 9bc604d253a..df61c36b579 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -73,12 +73,6 @@ Common::Rect getRoomObjectHotspotBounds(const ObjectRecord &object);
 bool shouldDisplaySceneAnimation(const AnimRecord &anim);
 bool loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resources, SceneState &scene);
 bool shouldRunStartupRoomProbe();
-const ObjectRecord *findSceneObjectByName(const Common::Array<ObjectRecord> &objects,
-	const Common::String &objectName);
-const RegionRecord *findSceneRegionByName(const Common::Array<RegionRecord> &regions,
-	const Common::String &regionName);
-ObjectRecord *findSceneObjectByName(Common::Array<ObjectRecord> &objects,
-	const Common::String &objectName);
 bool unlocksRoomObjectInteractionAfterInitialExamine(const ObjectRecord &object,
 	Script &script);
 bool doesPlayerFacingMatchRegion(int playerFacing, const RegionRecord &region);
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index b2de54442b2..6b64240b16d 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -47,6 +47,7 @@
 #include "harvester/input.h"
 #include "harvester/room_presentation.h"
 #include "harvester/room_support.h"
+#include "harvester/scene_entities.h"
 
 namespace Harvester {
 
@@ -434,7 +435,7 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
 			return Common::String();
 		if (target == playerState.entity)
 			return Common::String(kPlayerInventoryLabel);
-		if (const ObjectRecord *object = findSceneObjectByName(_scene.sceneObjects, target->getName()))
+		if (const ObjectRecord *object = SceneEntities::findSceneObjectByName(_scene.sceneObjects, target->getName()))
 			return _engine.getScript()->resolveObjectLabel(*object);
 		for (const NpcRecord &npc : _scene.state.roomNpcs) {
 			if (npc.npcName.equalsIgnoreCase(target->getName()))
diff --git a/engines/harvester/scene_entities.cpp b/engines/harvester/scene_entities.cpp
index fef3d4a21f6..3e90b24aa81 100644
--- a/engines/harvester/scene_entities.cpp
+++ b/engines/harvester/scene_entities.cpp
@@ -35,19 +35,51 @@ static bool sameRect(const Common::Rect &lhs, const Common::Rect &rhs) {
 		lhs.right == rhs.right && lhs.bottom == rhs.bottom;
 }
 
-static const AnimRecord *findRoomAnimByNameConst(const Common::Array<AnimRecord> &anims, const Common::String &animName) {
-	for (const AnimRecord &anim : anims) {
-		if (anim.animName.equalsIgnoreCase(animName))
-			return &anim;
+const ObjectRecord *SceneEntities::findSceneObjectByName(const Common::Array<ObjectRecord> &objects,
+		const Common::String &objectName) {
+	for (const ObjectRecord &object : objects) {
+		if (object.objectName.equalsIgnoreCase(objectName))
+			return &object;
+	}
+
+	return nullptr;
+}
+
+const RegionRecord *SceneEntities::findSceneRegionByName(const Common::Array<RegionRecord> &regions,
+		const Common::String &regionName) {
+	for (const RegionRecord &region : regions) {
+		if (region.regionName.equalsIgnoreCase(regionName))
+			return ®ion;
 	}
+
 	return nullptr;
 }
 
-static const NpcRecord *findRoomNpcByNameConst(const Common::Array<NpcRecord> &npcs, const Common::String &npcName) {
+const NpcRecord *SceneEntities::findSceneNpcByName(const Common::Array<NpcRecord> &npcs,
+		const Common::String &npcName) {
 	for (const NpcRecord &npc : npcs) {
 		if (npc.npcName.equalsIgnoreCase(npcName))
 			return &npc;
 	}
+
+	return nullptr;
+}
+
+ObjectRecord *SceneEntities::findSceneObjectByName(Common::Array<ObjectRecord> &objects,
+		const Common::String &objectName) {
+	for (ObjectRecord &object : objects) {
+		if (object.objectName.equalsIgnoreCase(objectName))
+			return &object;
+	}
+
+	return nullptr;
+}
+
+static const AnimRecord *findRoomAnimByNameConst(const Common::Array<AnimRecord> &anims, const Common::String &animName) {
+	for (const AnimRecord &anim : anims) {
+		if (anim.animName.equalsIgnoreCase(animName))
+			return &anim;
+	}
 	return nullptr;
 }
 
@@ -270,7 +302,7 @@ bool SceneEntities::reconcileSceneEntitiesInPlace(const SceneState &previousScen
 
 	for (const NpcRecord &npc : _state.roomNpcs) {
 		const NpcRecord *updatedNpc =
-			findRoomNpcByNameConst(updatedState.roomNpcs, npc.npcName);
+			findSceneNpcByName(updatedState.roomNpcs, npc.npcName);
 		const bool keepCurrentRoomDeathEntity =
 			!updatedNpc &&
 			isRetainedCurrentRoomNpcDeathRecord(npc) &&
@@ -298,7 +330,7 @@ bool SceneEntities::reconcileSceneEntitiesInPlace(const SceneState &previousScen
 			? _entityManager->findSceneEntityByName(npc.npcName)
 			: nullptr;
 		const NpcRecord *previousNpc =
-			findRoomNpcByNameConst(_state.roomNpcs, npc.npcName);
+			findSceneNpcByName(_state.roomNpcs, npc.npcName);
 		const bool keepQueuedDeathEntity =
 			entity &&
 			previousNpc &&
diff --git a/engines/harvester/scene_entities.h b/engines/harvester/scene_entities.h
index 9bcc84f5679..5d751fa5454 100644
--- a/engines/harvester/scene_entities.h
+++ b/engines/harvester/scene_entities.h
@@ -44,6 +44,15 @@ public:
 	void populateRoomMonsters();
 	void restoreRoomMonsterAnimations();
 
+	static const ObjectRecord *findSceneObjectByName(const Common::Array<ObjectRecord> &objects,
+		const Common::String &objectName);
+	static ObjectRecord *findSceneObjectByName(Common::Array<ObjectRecord> &objects,
+		const Common::String &objectName);
+	static const RegionRecord *findSceneRegionByName(const Common::Array<RegionRecord> &regions,
+		const Common::String &regionName);
+	static const NpcRecord *findSceneNpcByName(const Common::Array<NpcRecord> &npcs,
+		const Common::String &npcName);
+
 	void removeSceneEntityByName(const Common::String &entityName);
 	static Common::Rect getSceneObjectBounds(const ObjectRecord &object);
 	static Common::String resolveSceneObjectSpritePathLocal(const ObjectRecord &object);


Commit: fb53fc98380ae9e17538cd99559e8e1ad97871d6
    https://github.com/scummvm/scummvm/commit/fb53fc98380ae9e17538cd99559e8e1ad97871d6
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:08-04:00

Commit Message:
HARVESTER: Move scene resource assembly into SceneEntities

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/art.cpp
    engines/harvester/art.h
    engines/harvester/flow.cpp
    engines/harvester/room_support.h
    engines/harvester/scene.cpp
    engines/harvester/scene_entities.cpp
    engines/harvester/scene_entities.h


diff --git a/engines/harvester/art.cpp b/engines/harvester/art.cpp
index 702c04b1966..63ec43b2851 100644
--- a/engines/harvester/art.cpp
+++ b/engines/harvester/art.cpp
@@ -93,6 +93,27 @@ static const char *const kAmmoIconPaths[] = {
 
 } // End of anonymous namespace
 
+bool Art::loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *dest) {
+	Common::Array<byte> data;
+	if (!resources.loadFile(path, data) || data.size() < 256 * 3)
+		return false;
+
+	memcpy(dest, data.data(), 256 * 3);
+
+	byte minValue = 0;
+	byte maxValue = 0;
+	getPaletteByteRange(dest, minValue, maxValue);
+	debugC(3, kDebugRoom,
+		"Harvester: decoded room palette '%s' bytes=%u range=[%u,%u] hash=%08x idx0=(%u,%u,%u) idx1=(%u,%u,%u) idx127=(%u,%u,%u) idx255=(%u,%u,%u)",
+		path.c_str(), (uint)data.size(), minValue, maxValue, hashPalette(dest),
+		dest[0], dest[1], dest[2],
+		dest[3], dest[4], dest[5],
+		dest[127 * 3], dest[127 * 3 + 1], dest[127 * 3 + 2],
+		dest[255 * 3], dest[255 * 3 + 1], dest[255 * 3 + 2]);
+
+	return true;
+}
+
 bool Art::load(ResourceManager &resources) {
 	_waitFrames.clear();
 	_textboxes.clear();
diff --git a/engines/harvester/art.h b/engines/harvester/art.h
index 1b810560a21..47192e73ccd 100644
--- a/engines/harvester/art.h
+++ b/engines/harvester/art.h
@@ -36,6 +36,7 @@ class Art {
 public:
 	bool load(ResourceManager &resources);
 	bool loadQuickTipsResources(ResourceManager &resources, bool useTextboxPanel);
+	static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *dest);
 	void drawWaitFrame(Graphics::Screen &screen) const;
 
 	const byte *getWaitPalette() const { return _waitPalette; }
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index a43ae5f9721..17d3b4c1058 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -241,26 +241,6 @@ static bool applyStartupActorPlacement(const RoomSetupState &state, Entity &enti
 		state.playerSpawnX, state.playerSpawnY, (float)state.playerSpawnZ, &state.entranceName);
 }
 
-static void getPaletteByteRange(const byte *palette, byte &minValue, byte &maxValue) {
-	minValue = 255;
-	maxValue = 0;
-
-	for (uint i = 0; i < 256 * 3; ++i) {
-		minValue = MIN(minValue, palette[i]);
-		maxValue = MAX(maxValue, palette[i]);
-	}
-}
-
-static uint32 hashPalette(const byte *palette) {
-	uint32 hash = 2166136261U;
-	for (uint i = 0; i < 256 * 3; ++i) {
-		hash ^= palette[i];
-		hash *= 16777619U;
-	}
-
-	return hash;
-}
-
 void logScenePaletteSummary(const char *label, const SceneState &scene, float brightness) {
 	debugC(2, kDebugRoom, "Harvester: %s room='%s' palette='%s' brightness=%.2f",
 		label, scene.state.roomName.c_str(), scene.state.palettePath.c_str(), (double)brightness);
@@ -294,34 +274,6 @@ static void setScaledDisplayPalette(Graphics::Screen &screen, const byte *palett
 	screen.setPalette(scaledPalette);
 }
 
-static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *dest) {
-	Common::Array<byte> data;
-	if (!resources.loadFile(path, data) || data.size() < 256 * 3)
-		return false;
-
-	memcpy(dest, data.data(), 256 * 3);
-
-	byte minValue = 0;
-	byte maxValue = 0;
-	getPaletteByteRange(dest, minValue, maxValue);
-	debugC(3, kDebugRoom,
-		"Harvester: decoded room palette '%s' bytes=%u range=[%u,%u] hash=%08x idx0=(%u,%u,%u) idx1=(%u,%u,%u) idx127=(%u,%u,%u) idx255=(%u,%u,%u)",
-		path.c_str(), (uint)data.size(), minValue, maxValue, hashPalette(dest),
-		dest[0], dest[1], dest[2],
-		dest[3], dest[4], dest[5],
-		dest[127 * 3], dest[127 * 3 + 1], dest[127 * 3 + 2],
-		dest[255 * 3], dest[255 * 3 + 1], dest[255 * 3 + 2]);
-
-	return true;
-}
-
-Common::Rect getRoomObjectHotspotBounds(const ObjectRecord &object) {
-	if (object.boundsX2 > object.currentX && object.boundsY2 > object.currentY)
-		return Common::Rect(object.currentX, object.currentY, object.boundsX2 + 1, object.boundsY2 + 1);
-
-	return Common::Rect();
-}
-
 static Common::Rect getRegionBounds(const RegionRecord &region) {
 	if (region.right > region.left && region.bottom > region.top)
 		return Common::Rect(region.left, region.top, region.right, region.bottom);
@@ -329,17 +281,6 @@ static Common::Rect getRegionBounds(const RegionRecord &region) {
 	return Common::Rect();
 }
 
-Common::String resolveSceneObjectSpritePath(const ObjectRecord &object) {
-	const bool atInitialPlacement = object.currentX == object.initialX &&
-		object.currentY == object.initialY &&
-		object.currentOwnerOrRoom.equalsIgnoreCase(object.initialOwnerOrRoom);
-	if (!object.altSpritePath.empty() &&
-		(!atInitialPlacement || object.currentOwnerOrRoom.equalsIgnoreCase("INVENTORY")))
-		return object.altSpritePath;
-
-	return object.spritePath;
-}
-
 bool loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap) {
 	Common::Array<byte> data;
 	if (!resources.loadFile(path, data) || data.size() < 12)
@@ -349,77 +290,6 @@ bool loadBitmapResource(ResourceManager &resources, const Common::String &path,
 	return decodeBitmap(data, bitmap);
 }
 
-static void logSceneObjectSelection(const char *decision, const char *source, const ObjectRecord &object,
-		const Common::String &detail = Common::String()) {
-	const Common::Rect hotspotBounds = getRoomObjectHotspotBounds(object);
-	const Common::String resolvedSpritePath = resolveSceneObjectSpritePath(object);
-	debugC(3, kDebugScene,
-		"Harvester: scene object %s source='%s' object='%s' owner='%s' visible=%d runtimeVisible=%d sprite='%s' alt='%s' resolved='%s' pos=(%d,%d,%d) bounds=(%d,%d)-(%d,%d) action='%s' detail='%s'",
-		decision, source, object.objectName.c_str(), object.currentOwnerOrRoom.c_str(),
-		object.visible, object.runtimeVisible, object.spritePath.c_str(), object.altSpritePath.c_str(),
-		resolvedSpritePath.c_str(),
-		object.currentX, object.currentY, object.currentZ,
-		hotspotBounds.left, hotspotBounds.top, hotspotBounds.right, hotspotBounds.bottom,
-		object.actionTag.c_str(), detail.c_str());
-}
-
-static bool shouldQueueSceneObject(const ObjectRecord &object) {
-	return object.visible;
-}
-
-bool shouldDisplaySceneAnimation(const AnimRecord &anim) {
-	return anim.active || anim.visible;
-}
-
-static void queueSceneObject(const char *source, const ObjectRecord &object,
-		Common::Array<ObjectRecord> &sceneObjects) {
-	if (!shouldQueueSceneObject(object)) {
-		logSceneObjectSelection("skipped", source, object, object.visible ? "inactive" : "visible=0");
-		return;
-	}
-
-	for (const ObjectRecord &sceneObject : sceneObjects) {
-		if (sceneObject.currentOwnerOrRoom.equalsIgnoreCase(object.currentOwnerOrRoom) &&
-			sceneObject.objectName.equalsIgnoreCase(object.objectName)) {
-			logSceneObjectSelection("skipped", source, object, "duplicate owner/object");
-			return;
-		}
-	}
-
-	sceneObjects.push_back(object);
-	logSceneObjectSelection("queued", source, object);
-}
-
-bool loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resources, SceneState &scene) {
-	scene = SceneState();
-	scene.state = state;
-	scene.targetPaletteBrightness = state.paletteBrightness;
-	if (!loadPaletteResource(resources, state.palettePath, scene.palette)) {
-		return false;
-	}
-
-	Common::Array<ObjectRecord> sceneObjects;
-	for (const ObjectRecord &object : state.roomObjects)
-		queueSceneObject("room", object, sceneObjects);
-	for (const ObjectRecord &object : state.activeObjects)
-		queueSceneObject("active", object, sceneObjects);
-
-	scene.sceneObjects = Common::move(sceneObjects);
-	for (const AnimRecord &anim : state.roomAnimations) {
-		if (shouldDisplaySceneAnimation(anim))
-			scene.sceneAnimations.push_back(anim);
-	}
-	scene.sceneRegions = state.roomRegions;
-
-	debugC(2, kDebugRoom,
-		"Harvester: room scene loaded room='%s' objects=%u anims=%u npcs=%u monsters=%u timers=%u regions=%u",
-		state.roomName.c_str(), (uint)scene.sceneObjects.size(), (uint)scene.sceneAnimations.size(),
-		(uint)state.roomNpcs.size(), (uint)state.roomMonsters.size(),
-		(uint)state.roomTimers.size(), (uint)scene.sceneRegions.size());
-
-	return true;
-}
-
 bool shouldRunStartupRoomProbe() {
 	return ConfMan.hasKey("harvester_debug_probe_startup_room") &&
 		ConfMan.getBool("harvester_debug_probe_startup_room");
@@ -701,7 +571,7 @@ RoomHoverState Flow::getRoomHoverState(const SceneState &scene) const {
 
 static bool findRoomObjectProbePoint(HarvesterEngine &engine, const Common::Array<ObjectRecord> &sceneObjects,
 		const ObjectRecord &object, Common::Point &probePoint) {
-	const Common::Rect bounds = getRoomObjectHotspotBounds(object);
+	const Common::Rect bounds = SceneEntities::getSceneObjectBounds(object);
 	if (bounds.isEmpty())
 		return false;
 
@@ -769,7 +639,7 @@ void Flow::logStartupRoomProbe(const SceneState &scene, const Common::String &en
 	}
 
 	for (const ObjectRecord &object : scene.sceneObjects) {
-		if (getRoomObjectHotspotBounds(object).isEmpty())
+		if (SceneEntities::getSceneObjectBounds(object).isEmpty())
 			continue;
 
 		Common::Point probePoint;
@@ -864,7 +734,7 @@ static bool loadQuickTipsScene(HarvesterEngine &engine, SceneState &scene) {
 	if (!engine.getScript()->resolveRoomSetupState("START", state, *engine.getResources()))
 		return false;
 
-	return loadRoomSceneResources(state, *engine.getResources(), scene);
+	return SceneEntities::loadRoomSceneResources(state, *engine.getResources(), scene);
 }
 
 static void renderQuickTipsScreen(HarvesterEngine &engine, const SceneState &scene,
@@ -1151,7 +1021,7 @@ Common::Error Flow::runDemoEnding() {
 		IndexedBitmap bitmap;
 		byte palette[256 * 3];
 		if (!loadBitmapResource(*resources, bitmapPath, bitmap) ||
-				!loadPaletteResource(*resources, palettePath, palette)) {
+				!Art::loadPaletteResource(*resources, palettePath, palette)) {
 			warning("Harvester: unable to load DOS demo ending screen %d", screenNumber);
 			return Common::kReadingFailed;
 		}
@@ -1212,7 +1082,7 @@ Common::Error Flow::runDemoIntroduction() {
 	IndexedBitmap bitmap;
 	byte palette[256 * 3];
 	if (!loadBitmapResource(*resources, kDemoIntroductionBitmapPath, bitmap) ||
-			!loadPaletteResource(*resources, kDemoIntroductionPalettePath, palette)) {
+			!Art::loadPaletteResource(*resources, kDemoIntroductionPalettePath, palette)) {
 		return Common::kReadingFailed;
 	}
 
@@ -1355,7 +1225,7 @@ Common::Error Flow::runTownMapSelector(const Common::String &mapEntryName,
 		: kTownMapNightPaletteBrightness;
 
 	byte palette[256 * 3];
-	if (!loadPaletteResource(*resources, kTownMapPalettePath, palette))
+	if (!Art::loadPaletteResource(*resources, kTownMapPalettePath, palette))
 		return Common::kReadingFailed;
 
 	Common::Array<IndexedBitmap> panels;
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index df61c36b579..35f6b22dddd 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -69,9 +69,7 @@ bool applyRoomActorPlacement(const RoomSetupState &state, Entity &entity,
 	int centerX, int bottomY, float z, const Common::String *entranceName = nullptr);
 void logScenePaletteSummary(const char *label, const SceneState &scene, float brightness);
 bool captureScreenBackdrop(const Graphics::Screen &screen, IndexedBitmap &bitmap);
-Common::Rect getRoomObjectHotspotBounds(const ObjectRecord &object);
 bool shouldDisplaySceneAnimation(const AnimRecord &anim);
-bool loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resources, SceneState &scene);
 bool shouldRunStartupRoomProbe();
 bool unlocksRoomObjectInteractionAfterInitialExamine(const ObjectRecord &object,
 	Script &script);
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index 6b64240b16d..baaac7fd858 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -976,7 +976,7 @@ Common::Error SceneRunner::runRoomLoop(Flow &flow, const Common::String &targetN
 			return transitionError;
 
 		SceneState scene;
-		if (!loadRoomSceneResources(state, *_engine.getResources(), scene))
+		if (!SceneEntities::loadRoomSceneResources(state, *_engine.getResources(), scene))
 			return Common::kReadingFailed;
 
 		Graphics::Screen *screen = _engine.getScreen();
diff --git a/engines/harvester/scene_entities.cpp b/engines/harvester/scene_entities.cpp
index 3e90b24aa81..c01b91c5df5 100644
--- a/engines/harvester/scene_entities.cpp
+++ b/engines/harvester/scene_entities.cpp
@@ -21,7 +21,9 @@
 
 #include "harvester/scene_entities.h"
 
+#include "harvester/art.h"
 #include "harvester/detection.h"
+#include "harvester/resources.h"
 #include "harvester/monster.h"
 #include "harvester/player.h"
 #include "harvester/combat.h"
@@ -172,6 +174,77 @@ SceneEntities::SceneEntities(RoomSetupState &state, Script *script, ResourceMana
 		_state.roomName.c_str(), _entityManager != nullptr);
 }
 
+static void logSceneObjectSelection(const char *decision, const char *source, const ObjectRecord &object,
+		const Common::String &detail = Common::String()) {
+	const Common::Rect hotspotBounds = SceneEntities::getSceneObjectBounds(object);
+	const Common::String resolvedSpritePath = SceneEntities::resolveSceneObjectSpritePathLocal(object);
+	debugC(3, kDebugScene,
+		"Harvester: scene object %s source='%s' object='%s' owner='%s' visible=%d runtimeVisible=%d sprite='%s' alt='%s' resolved='%s' pos=(%d,%d,%d) bounds=(%d,%d)-(%d,%d) action='%s' detail='%s'",
+		decision, source, object.objectName.c_str(), object.currentOwnerOrRoom.c_str(),
+		object.visible, object.runtimeVisible, object.spritePath.c_str(), object.altSpritePath.c_str(),
+		resolvedSpritePath.c_str(),
+		object.currentX, object.currentY, object.currentZ,
+		hotspotBounds.left, hotspotBounds.top, hotspotBounds.right, hotspotBounds.bottom,
+		object.actionTag.c_str(), detail.c_str());
+}
+
+static bool shouldQueueSceneObject(const ObjectRecord &object) {
+	return object.visible;
+}
+
+bool shouldDisplaySceneAnimation(const AnimRecord &anim) {
+	return anim.active || anim.visible;
+}
+
+static void queueSceneObject(const char *source, const ObjectRecord &object,
+		Common::Array<ObjectRecord> &sceneObjects) {
+	if (!shouldQueueSceneObject(object)) {
+		logSceneObjectSelection("skipped", source, object, object.visible ? "inactive" : "visible=0");
+		return;
+	}
+
+	for (const ObjectRecord &sceneObject : sceneObjects) {
+		if (sceneObject.currentOwnerOrRoom.equalsIgnoreCase(object.currentOwnerOrRoom) &&
+			sceneObject.objectName.equalsIgnoreCase(object.objectName)) {
+			logSceneObjectSelection("skipped", source, object, "duplicate owner/object");
+			return;
+		}
+	}
+
+	sceneObjects.push_back(object);
+	logSceneObjectSelection("queued", source, object);
+}
+
+bool SceneEntities::loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resources, SceneState &scene) {
+	scene = SceneState();
+	scene.state = state;
+	scene.targetPaletteBrightness = state.paletteBrightness;
+	if (!Art::loadPaletteResource(resources, state.palettePath, scene.palette)) {
+		return false;
+	}
+
+	Common::Array<ObjectRecord> sceneObjects;
+	for (const ObjectRecord &object : state.roomObjects)
+		queueSceneObject("room", object, sceneObjects);
+	for (const ObjectRecord &object : state.activeObjects)
+		queueSceneObject("active", object, sceneObjects);
+
+	scene.sceneObjects = Common::move(sceneObjects);
+	for (const AnimRecord &anim : state.roomAnimations) {
+		if (shouldDisplaySceneAnimation(anim))
+			scene.sceneAnimations.push_back(anim);
+	}
+	scene.sceneRegions = state.roomRegions;
+
+	debugC(2, kDebugRoom,
+		"Harvester: room scene loaded room='%s' objects=%u anims=%u npcs=%u monsters=%u timers=%u regions=%u",
+		state.roomName.c_str(), (uint)scene.sceneObjects.size(), (uint)scene.sceneAnimations.size(),
+		(uint)state.roomNpcs.size(), (uint)state.roomMonsters.size(),
+		(uint)state.roomTimers.size(), (uint)scene.sceneRegions.size());
+
+	return true;
+}
+
 bool SceneEntities::loadSceneResources(const RoomSetupState &state, SceneState &scene) const {
 	return _resources && loadRoomSceneResources(state, *_resources, scene);
 }
@@ -741,7 +814,7 @@ void SceneEntities::populateRoomObjects(const Common::Array<ObjectRecord> &drawa
 
 	for (const ObjectRecord &object : drawableObjects) {
 		const Common::String spritePath = resolveSceneObjectSpritePathLocal(object);
-		const Common::Rect hotspotBounds = getRoomObjectHotspotBounds(object);
+		const Common::Rect hotspotBounds = getSceneObjectBounds(object);
 		Entity *entity = createSceneObjectEntity(object, spritePath, hotspotBounds);
 
 		if (!entity) {
diff --git a/engines/harvester/scene_entities.h b/engines/harvester/scene_entities.h
index 5d751fa5454..c6ef70c4154 100644
--- a/engines/harvester/scene_entities.h
+++ b/engines/harvester/scene_entities.h
@@ -32,6 +32,7 @@ class SceneEntities {
 public:
 	SceneEntities(RoomSetupState &state, Script *script, ResourceManager *resources, EntityManager *entityManager);
 
+	static bool loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resources, SceneState &scene);
 	bool loadSceneResources(const RoomSetupState &state, SceneState &scene) const;
 	bool reconcileSceneEntitiesInPlace(const SceneState &previousScene,
 		RoomSetupState &updatedState, const SceneState &updatedScene);


Commit: bdb15ec5f3c36352984551f03bb0928024206031
    https://github.com/scummvm/scummvm/commit/bdb15ec5f3c36352984551f03bb0928024206031
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:08-04:00

Commit Message:
HARVESTER: Move hover resolution into Input

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/flow.cpp
    engines/harvester/input.cpp
    engines/harvester/input.h
    engines/harvester/room_support.h


diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 17d3b4c1058..99a8e1073a7 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -39,6 +39,7 @@
 #include "harvester/detection.h"
 #include "harvester/fst_player.h"
 #include "harvester/harvester.h"
+#include "harvester/input.h"
 #include "harvester/palette_utils.h"
 #include "harvester/resources.h"
 #include "harvester/entity.h"
@@ -101,12 +102,6 @@ static const int kTownMapCursorHitExtent = 5;
 static const byte kTownMapLabelColor = 0x28;
 
 static const int kCursorSequenceWalk = 0;
-static const int kCursorSequenceExamine = 1;
-static const int kCursorSequenceTalk = 2;
-static const int kCursorSequenceOperate = 4;
-static const int kCursorSequencePickup = 5;
-static const int kCursorSequenceTransition = 6;
-static const int kCursorSequenceNeutral = 7;
 } // End of anonymous namespace
 
 static int clampTownMapPanelIndex(int panelIndex) {
@@ -295,194 +290,6 @@ bool shouldRunStartupRoomProbe() {
 		ConfMan.getBool("harvester_debug_probe_startup_room");
 }
 
-static bool canTalkToRoomNpc(HarvesterEngine &engine, const NpcRecord &npc,
-		const DialogueSystem *dialogue) {
-	Script *script = engine.getScript();
-	if (!script || !script->isNamedNpcDeathTypeClear(npc.npcName))
-		return false;
-
-	return dialogue && dialogue->hasRoomNpcHandler(npc.npcName);
-}
-
-static const ObjectRecord *findRoomObjectAtPoint(HarvesterEngine &engine,
-		const Common::Array<ObjectRecord> &sceneObjects, const Common::Point &point) {
-	EntityManager *entityManager = engine.getRuntimeEntities();
-	if (!entityManager)
-		return nullptr;
-
-	const Entity *topEntity = nullptr;
-	const ObjectRecord *topObject = nullptr;
-	int topDrawIndex = -1;
-	for (const ObjectRecord &object : sceneObjects) {
-		if (object.objectName.empty())
-			continue;
-
-		const Entity *entity = entityManager->findSceneEntityByName(object.objectName);
-		if (!entity || !entity->hitTest(point))
-			continue;
-		if (entity->getClassId() == kEntityClassBackground ||
-			entity->getClassId() == kEntityClassPlayer ||
-			entity->getClassId() == kEntityClassRectHotspot19) {
-			continue;
-		}
-		const int drawIndex = entityManager->findSceneEntityDrawIndexByName(entity->getName());
-		if (drawIndex < 0)
-			continue;
-		const bool objectVisible = object.visible && object.runtimeVisible;
-		const bool topObjectVisible = topObject && topObject->visible && topObject->runtimeVisible;
-		if (!topEntity ||
-			(objectVisible && !topObjectVisible) ||
-			(objectVisible == topObjectVisible && drawIndex > topDrawIndex)) {
-			topEntity = entity;
-			topObject = &object;
-			topDrawIndex = drawIndex;
-		}
-	}
-	if (!topEntity)
-		return nullptr;
-
-	return topObject;
-}
-
-static const RegionRecord *findRoomRegionAtPoint(HarvesterEngine &engine,
-		const Common::Array<RegionRecord> &sceneRegions, const Common::Point &point) {
-	EntityManager *entityManager = engine.getRuntimeEntities();
-	if (!entityManager)
-		return nullptr;
-
-	const Entity *topEntity = nullptr;
-	int topDrawIndex = -1;
-	for (const RegionRecord &region : sceneRegions) {
-		const Entity *entity = entityManager->findSceneEntityByName(region.regionName);
-		if (!entity || !entity->hitTest(point))
-			continue;
-		if (entity->getClassId() != kEntityClassRectHotspot19)
-			continue;
-		const int drawIndex = entityManager->findSceneEntityDrawIndexByName(entity->getName());
-		if (drawIndex < 0)
-			continue;
-		if (!topEntity || drawIndex > topDrawIndex) {
-			topEntity = entity;
-			topDrawIndex = drawIndex;
-		}
-	}
-	if (!topEntity)
-		return nullptr;
-
-	return SceneEntities::findSceneRegionByName(sceneRegions, topEntity->getName());
-}
-
-static const Entity *findRoomPlayerAtPoint(HarvesterEngine &engine, const Common::Point &point) {
-	EntityManager *entityManager = engine.getRuntimeEntities();
-	if (!entityManager)
-		return nullptr;
-
-	const Entity *entity = entityManager->findTopSceneEntityAt(point);
-	if (!entity || entity->getClassId() != kEntityClassPlayer)
-		return nullptr;
-
-	return entity;
-}
-
-static const NpcRecord *findRoomNpcAtPoint(HarvesterEngine &engine,
-		const Common::Array<NpcRecord> &sceneNpcs, const Common::Point &point,
-		const DialogueSystem *dialogue) {
-	EntityManager *entityManager = engine.getRuntimeEntities();
-	if (!entityManager)
-		return nullptr;
-
-	const Entity *entity = entityManager->findTopSceneEntityAt(point);
-	if (!entity || entity->getClassId() != kEntityClassNpc)
-		return nullptr;
-
-	const NpcRecord *npc = SceneEntities::findSceneNpcByName(sceneNpcs, entity->getName());
-	if (!npc || !canTalkToRoomNpc(engine, *npc, dialogue))
-		return nullptr;
-
-	return npc;
-}
-
-static bool suppressesInitialObjectInspectGate(const ObjectRecord &object) {
-	// Native HARVEST.SCR uses ident key X as a no-text sentinel.
-	return object.identTextKey.equalsIgnoreCase("X");
-}
-
-static bool usesBareOperatePrompt(const ObjectRecord &object) {
-	return object.objectName.equalsIgnoreCase("HAPPLY_HS") ||
-		object.objectName.equalsIgnoreCase("KILL_STEPH_HS");
-}
-
-bool unlocksRoomObjectInteractionAfterInitialExamine(const ObjectRecord &object,
-		Script &script) {
-	if (suppressesInitialObjectInspectGate(object))
-		return false;
-
-	return object.operatable || script.isPickupObject(object);
-}
-
-static int resolveRoomObjectCursorSequence(const ObjectRecord &object, Script &script) {
-	if (object.objectName.equalsIgnoreCase("EXIT_BM") || object.objectName.equalsIgnoreCase("EXIT_HS"))
-		return kCursorSequenceTransition;
-
-	ResolvedText inspectText;
-	const bool pickupObject = script.isPickupObject(object);
-	const bool pickupBlocked = pickupObject && script.isPickupBlockedByAction(object);
-	if (!object.identShown && unlocksRoomObjectInteractionAfterInitialExamine(object, script))
-		return kCursorSequenceExamine;
-	if (pickupObject && !pickupBlocked)
-		return kCursorSequencePickup;
-	if (pickupBlocked)
-		return kCursorSequenceExamine;
-	if (object.operatable)
-		return kCursorSequenceOperate;
-	if (script.resolveObjectInspectText(object, inspectText))
-		return kCursorSequenceExamine;
-
-	if (script.hasObjectInteraction(object))
-		return kCursorSequenceExamine;
-
-	return kCursorSequenceNeutral;
-}
-
-static Common::String buildRoomObjectPrompt(const ObjectRecord &object, Script &script,
-		int cursorSequence, const MenuTextConfig &menuTextConfig) {
-	// Native room prompts come from explicit interaction metadata. Neutral scene sprites
-	// should not surface synthetic "Examine <object id>" prompts or steal hover/click focus.
-	if (cursorSequence == kCursorSequenceNeutral)
-		return Common::String();
-
-	const Common::String label = script.resolveObjectLabel(object);
-	if (label.empty())
-		return Common::String();
-
-	if (cursorSequence == kCursorSequenceOperate) {
-		if (usesBareOperatePrompt(object))
-			return label;
-		return Common::String::format("%s %s", menuTextConfig.operateVerb.c_str(), label.c_str());
-	}
-	if (cursorSequence == kCursorSequencePickup)
-		return Common::String::format("%s %s", menuTextConfig.pickUpVerb.c_str(), label.c_str());
-	if (cursorSequence == kCursorSequenceTalk)
-		return Common::String::format("%s %s", menuTextConfig.talkToVerb.c_str(), label.c_str());
-	if (cursorSequence == kCursorSequenceTransition)
-		return Common::String::format("Go to %s", label.c_str());
-
-	return Common::String::format("%s %s", menuTextConfig.examineTheVerb.c_str(), label.c_str());
-}
-
-static Common::String buildRoomNpcPrompt(const NpcRecord &npc,
-		const MenuTextConfig &menuTextConfig) {
-	Common::String label = !npc.entityInitArg.empty() ? npc.entityInitArg : npc.npcName;
-	for (uint i = 0; i < label.size(); ++i) {
-		if (label[i] == '_')
-			label.setChar(' ', i);
-	}
-
-	if (label.empty())
-		return Common::String();
-	return Common::String::format("%s %s", menuTextConfig.talkToVerb.c_str(), label.c_str());
-}
-
 bool doesPlayerFacingMatchRegion(int playerFacing, const RegionRecord &region) {
 	return region.desiredFacing < 0 || playerFacing == region.desiredFacing;
 }
@@ -508,64 +315,8 @@ bool doesPlayerOverlapRegion(const Entity &playerEntity, const RegionRecord &reg
 	return playerMaxZ >= region.minZ && region.maxZ >= (int)playerEntity.getZ();
 }
 
-RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupState &state,
-		const Common::Array<ObjectRecord> &sceneObjects,
-		const Common::Array<NpcRecord> &sceneNpcs,
-		const Common::Array<RegionRecord> &sceneRegions, const Common::Point &mousePos,
-		const MenuTextConfig &menuTextConfig, const DialogueSystem *dialogue) {
-	RoomHoverState hoverState;
-	EntityManager *entityManager = engine.getRuntimeEntities();
-	if (const Entity *playerEntity = findRoomPlayerAtPoint(engine, mousePos)) {
-		hoverState.playerEntity = playerEntity;
-		hoverState.cursorSequence = kCursorSequenceExamine;
-		return hoverState;
-	}
-	if (const NpcRecord *npc = findRoomNpcAtPoint(engine, sceneNpcs, mousePos, dialogue)) {
-		hoverState.npc = npc;
-		hoverState.cursorSequence = kCursorSequenceTalk;
-		hoverState.promptText = buildRoomNpcPrompt(*npc, menuTextConfig);
-		return hoverState;
-	}
-
-	Script *script = engine.getScript();
-	if (!script)
-		return hoverState;
-	hoverState.object = findRoomObjectAtPoint(engine, sceneObjects, mousePos);
-	hoverState.region = findRoomRegionAtPoint(engine, sceneRegions, mousePos);
-	if (entityManager && hoverState.object && hoverState.region) {
-		const int objectDrawIndex =
-			entityManager->findSceneEntityDrawIndexByName(hoverState.object->objectName);
-		const int regionDrawIndex =
-			entityManager->findSceneEntityDrawIndexByName(hoverState.region->regionName);
-		if (regionDrawIndex >= 0 && objectDrawIndex >= 0 && regionDrawIndex > objectDrawIndex)
-			hoverState.object = nullptr;
-	}
-	if (hoverState.object) {
-		hoverState.cursorSequence = resolveRoomObjectCursorSequence(*hoverState.object, *script);
-		hoverState.promptText = buildRoomObjectPrompt(
-			*hoverState.object, *script, hoverState.cursorSequence, menuTextConfig);
-		if (hoverState.cursorSequence != kCursorSequenceNeutral || !hoverState.promptText.empty())
-			return hoverState;
-		hoverState.object = nullptr;
-	}
-	if (hoverState.region && hoverState.region->cursorEnabled) {
-		hoverState.object = nullptr;
-		hoverState.cursorSequence = kCursorSequenceTransition;
-		hoverState.promptText.clear();
-		return hoverState;
-	}
-
-	if (Player::supportsMovementBand(state) &&
-		mousePos.y >= state.roomMaxZScreenY &&
-		mousePos.y <= state.roomMinZScreenY) {
-		hoverState.cursorSequence = kCursorSequenceWalk;
-	}
-
-	return hoverState;
-}
-
 RoomHoverState Flow::getRoomHoverState(const SceneState &scene) const {
-	return resolveRoomHoverState(_engine, scene.state, scene.sceneObjects,
+	return Input::resolveRoomHoverState(_engine, scene.state, scene.sceneObjects,
 		scene.state.roomNpcs, scene.sceneRegions, _mousePos, _menuTextConfig, &_dialogue);
 }
 
@@ -584,7 +335,7 @@ static bool findRoomObjectProbePoint(HarvesterEngine &engine, const Common::Arra
 		Common::Point(bounds.right - 1, bounds.bottom - 1)
 	};
 	for (const Common::Point &candidate : corners) {
-		const ObjectRecord *hit = findRoomObjectAtPoint(engine, sceneObjects, candidate);
+		const ObjectRecord *hit = Input::findRoomObjectAtPoint(engine, sceneObjects, candidate);
 		if (hit && hit->objectName.equalsIgnoreCase(object.objectName)) {
 			probePoint = candidate;
 			return true;
@@ -596,7 +347,7 @@ static bool findRoomObjectProbePoint(HarvesterEngine &engine, const Common::Arra
 	for (int y = bounds.top; y < bounds.bottom; y += yStep) {
 		for (int x = bounds.left; x < bounds.right; x += xStep) {
 			const Common::Point candidate(x, y);
-			const ObjectRecord *hit = findRoomObjectAtPoint(engine, sceneObjects, candidate);
+			const ObjectRecord *hit = Input::findRoomObjectAtPoint(engine, sceneObjects, candidate);
 			if (hit && hit->objectName.equalsIgnoreCase(object.objectName)) {
 				probePoint = candidate;
 				return true;
@@ -646,14 +397,14 @@ void Flow::logStartupRoomProbe(const SceneState &scene, const Common::String &en
 		if (!findRoomObjectProbePoint(_engine, scene.sceneObjects, object, probePoint))
 			continue;
 
-		const ObjectRecord *hoveredObject = findRoomObjectAtPoint(_engine, scene.sceneObjects, probePoint);
+		const ObjectRecord *hoveredObject = Input::findRoomObjectAtPoint(_engine, scene.sceneObjects, probePoint);
 		if (!hoveredObject)
 			continue;
 
 		_mousePos = probePoint;
 		const Common::String objectLabel = script->resolveObjectLabel(*hoveredObject);
 		ResolvedText inspectText;
-		const RoomHoverState hoverState = resolveRoomHoverState(
+		const RoomHoverState hoverState = Input::resolveRoomHoverState(
 			_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
 			probePoint, _menuTextConfig);
 		const bool hasInteraction = script->hasObjectInteraction(*hoveredObject);
@@ -670,7 +421,7 @@ void Flow::logStartupRoomProbe(const SceneState &scene, const Common::String &en
 			bool foundFloorProbe = false;
 			for (int y = scene.state.roomMaxZScreenY; y <= scene.state.roomMinZScreenY && !foundFloorProbe; y += 12) {
 				for (int x = 48; x < 592; x += 16) {
-					const RoomHoverState candidateHover = resolveRoomHoverState(
+					const RoomHoverState candidateHover = Input::resolveRoomHoverState(
 						_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
 						Common::Point(x, y), _menuTextConfig);
 					if (candidateHover.cursorSequence == kCursorSequenceWalk) {
@@ -682,7 +433,7 @@ void Flow::logStartupRoomProbe(const SceneState &scene, const Common::String &en
 			}
 
 			const RoomHoverState floorHover = foundFloorProbe
-				? resolveRoomHoverState(_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
+				? Input::resolveRoomHoverState(_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
 					scene.sceneRegions, floorProbe, _menuTextConfig)
 				: RoomHoverState();
 			debugC(1, kDebugRoom,
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index dee80e80534..4a2122367f9 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -26,6 +26,7 @@
 #include "harvester/art.h"
 #include "harvester/cheats.h"
 #include "harvester/detection.h"
+#include "harvester/dialogue.h"
 #include "harvester/flow.h"
 #include "harvester/harvester.h"
 #include "harvester/inventory.h"
@@ -38,6 +39,14 @@
 
 namespace Harvester {
 
+static const int kCursorSequenceWalk = 0;
+static const int kCursorSequenceExamine = 1;
+static const int kCursorSequenceTalk = 2;
+static const int kCursorSequenceOperate = 4;
+static const int kCursorSequencePickup = 5;
+static const int kCursorSequenceTransition = 6;
+static const int kCursorSequenceNeutral = 7;
+
 static const uint32 kRoomExitFastClickWindowTicks = 20;
 static const int kNativeInventoryDragCloseLeft = 0x45;
 static const int kNativeInventoryDragCloseTop = 0x4b;
@@ -76,6 +85,250 @@ static void debugLogInventoryClick(const char *buttonLabel, const Common::Point
 		InventorySystem::isStatusObject(inventoryHover->object));
 }
 
+static bool canTalkToRoomNpc(HarvesterEngine &engine, const NpcRecord &npc,
+		const DialogueSystem *dialogue) {
+	Script *script = engine.getScript();
+	if (!script || !script->isNamedNpcDeathTypeClear(npc.npcName))
+		return false;
+
+	return dialogue && dialogue->hasRoomNpcHandler(npc.npcName);
+}
+
+const ObjectRecord *Input::findRoomObjectAtPoint(HarvesterEngine &engine,
+		const Common::Array<ObjectRecord> &sceneObjects, const Common::Point &point) {
+	EntityManager *entityManager = engine.getRuntimeEntities();
+	if (!entityManager)
+		return nullptr;
+
+	const Entity *topEntity = nullptr;
+	const ObjectRecord *topObject = nullptr;
+	int topDrawIndex = -1;
+	for (const ObjectRecord &object : sceneObjects) {
+		if (object.objectName.empty())
+			continue;
+
+		const Entity *entity = entityManager->findSceneEntityByName(object.objectName);
+		if (!entity || !entity->hitTest(point))
+			continue;
+		if (entity->getClassId() == kEntityClassBackground ||
+			entity->getClassId() == kEntityClassPlayer ||
+			entity->getClassId() == kEntityClassRectHotspot19) {
+			continue;
+		}
+		const int drawIndex = entityManager->findSceneEntityDrawIndexByName(entity->getName());
+		if (drawIndex < 0)
+			continue;
+		const bool objectVisible = object.visible && object.runtimeVisible;
+		const bool topObjectVisible = topObject && topObject->visible && topObject->runtimeVisible;
+		if (!topEntity ||
+			(objectVisible && !topObjectVisible) ||
+			(objectVisible == topObjectVisible && drawIndex > topDrawIndex)) {
+			topEntity = entity;
+			topObject = &object;
+			topDrawIndex = drawIndex;
+		}
+	}
+	if (!topEntity)
+		return nullptr;
+
+	return topObject;
+}
+
+static const RegionRecord *findRoomRegionAtPoint(HarvesterEngine &engine,
+		const Common::Array<RegionRecord> &sceneRegions, const Common::Point &point) {
+	EntityManager *entityManager = engine.getRuntimeEntities();
+	if (!entityManager)
+		return nullptr;
+
+	const Entity *topEntity = nullptr;
+	int topDrawIndex = -1;
+	for (const RegionRecord &region : sceneRegions) {
+		const Entity *entity = entityManager->findSceneEntityByName(region.regionName);
+		if (!entity || !entity->hitTest(point))
+			continue;
+		if (entity->getClassId() != kEntityClassRectHotspot19)
+			continue;
+		const int drawIndex = entityManager->findSceneEntityDrawIndexByName(entity->getName());
+		if (drawIndex < 0)
+			continue;
+		if (!topEntity || drawIndex > topDrawIndex) {
+			topEntity = entity;
+			topDrawIndex = drawIndex;
+		}
+	}
+	if (!topEntity)
+		return nullptr;
+
+	return SceneEntities::findSceneRegionByName(sceneRegions, topEntity->getName());
+}
+
+static const Entity *findRoomPlayerAtPoint(HarvesterEngine &engine, const Common::Point &point) {
+	EntityManager *entityManager = engine.getRuntimeEntities();
+	if (!entityManager)
+		return nullptr;
+
+	const Entity *entity = entityManager->findTopSceneEntityAt(point);
+	if (!entity || entity->getClassId() != kEntityClassPlayer)
+		return nullptr;
+
+	return entity;
+}
+
+static const NpcRecord *findRoomNpcAtPoint(HarvesterEngine &engine,
+		const Common::Array<NpcRecord> &sceneNpcs, const Common::Point &point,
+		const DialogueSystem *dialogue) {
+	EntityManager *entityManager = engine.getRuntimeEntities();
+	if (!entityManager)
+		return nullptr;
+
+	const Entity *entity = entityManager->findTopSceneEntityAt(point);
+	if (!entity || entity->getClassId() != kEntityClassNpc)
+		return nullptr;
+
+	const NpcRecord *npc = SceneEntities::findSceneNpcByName(sceneNpcs, entity->getName());
+	if (!npc || !canTalkToRoomNpc(engine, *npc, dialogue))
+		return nullptr;
+
+	return npc;
+}
+
+static bool suppressesInitialObjectInspectGate(const ObjectRecord &object) {
+	// Native HARVEST.SCR uses ident key X as a no-text sentinel.
+	return object.identTextKey.equalsIgnoreCase("X");
+}
+
+static bool usesBareOperatePrompt(const ObjectRecord &object) {
+	return object.objectName.equalsIgnoreCase("HAPPLY_HS") ||
+		object.objectName.equalsIgnoreCase("KILL_STEPH_HS");
+}
+
+static bool unlocksRoomObjectInteractionAfterInitialExamine(const ObjectRecord &object,
+		Script &script) {
+	if (suppressesInitialObjectInspectGate(object))
+		return false;
+
+	return object.operatable || script.isPickupObject(object);
+}
+
+static int resolveRoomObjectCursorSequence(const ObjectRecord &object, Script &script) {
+	if (object.objectName.equalsIgnoreCase("EXIT_BM") || object.objectName.equalsIgnoreCase("EXIT_HS"))
+		return kCursorSequenceTransition;
+
+	ResolvedText inspectText;
+	const bool pickupObject = script.isPickupObject(object);
+	const bool pickupBlocked = pickupObject && script.isPickupBlockedByAction(object);
+	if (!object.identShown && unlocksRoomObjectInteractionAfterInitialExamine(object, script))
+		return kCursorSequenceExamine;
+	if (pickupObject && !pickupBlocked)
+		return kCursorSequencePickup;
+	if (pickupBlocked)
+		return kCursorSequenceExamine;
+	if (object.operatable)
+		return kCursorSequenceOperate;
+	if (script.resolveObjectInspectText(object, inspectText))
+		return kCursorSequenceExamine;
+
+	if (script.hasObjectInteraction(object))
+		return kCursorSequenceExamine;
+
+	return kCursorSequenceNeutral;
+}
+
+static Common::String buildRoomObjectPrompt(const ObjectRecord &object, Script &script,
+		int cursorSequence, const MenuTextConfig &menuTextConfig) {
+	// Native room prompts come from explicit interaction metadata. Neutral scene sprites
+	// should not surface synthetic "Examine <object id>" prompts or steal hover/click focus.
+	if (cursorSequence == kCursorSequenceNeutral)
+		return Common::String();
+
+	const Common::String label = script.resolveObjectLabel(object);
+	if (label.empty())
+		return Common::String();
+
+	if (cursorSequence == kCursorSequenceOperate) {
+		if (usesBareOperatePrompt(object))
+			return label;
+		return Common::String::format("%s %s", menuTextConfig.operateVerb.c_str(), label.c_str());
+	}
+	if (cursorSequence == kCursorSequencePickup)
+		return Common::String::format("%s %s", menuTextConfig.pickUpVerb.c_str(), label.c_str());
+	if (cursorSequence == kCursorSequenceTalk)
+		return Common::String::format("%s %s", menuTextConfig.talkToVerb.c_str(), label.c_str());
+	if (cursorSequence == kCursorSequenceTransition)
+		return Common::String::format("Go to %s", label.c_str());
+
+	return Common::String::format("%s %s", menuTextConfig.examineTheVerb.c_str(), label.c_str());
+}
+
+static Common::String buildRoomNpcPrompt(const NpcRecord &npc,
+		const MenuTextConfig &menuTextConfig) {
+	Common::String label = !npc.entityInitArg.empty() ? npc.entityInitArg : npc.npcName;
+	for (uint i = 0; i < label.size(); ++i) {
+		if (label[i] == '_')
+			label.setChar(' ', i);
+	}
+
+	if (label.empty())
+		return Common::String();
+	return Common::String::format("%s %s", menuTextConfig.talkToVerb.c_str(), label.c_str());
+}
+
+RoomHoverState Input::resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupState &state,
+		const Common::Array<ObjectRecord> &sceneObjects,
+		const Common::Array<NpcRecord> &sceneNpcs,
+		const Common::Array<RegionRecord> &sceneRegions, const Common::Point &mousePos,
+		const MenuTextConfig &menuTextConfig, const DialogueSystem *dialogue) {
+	RoomHoverState hoverState;
+	EntityManager *entityManager = engine.getRuntimeEntities();
+	if (const Entity *playerEntity = findRoomPlayerAtPoint(engine, mousePos)) {
+		hoverState.playerEntity = playerEntity;
+		hoverState.cursorSequence = kCursorSequenceExamine;
+		return hoverState;
+	}
+	if (const NpcRecord *npc = findRoomNpcAtPoint(engine, sceneNpcs, mousePos, dialogue)) {
+		hoverState.npc = npc;
+		hoverState.cursorSequence = kCursorSequenceTalk;
+		hoverState.promptText = buildRoomNpcPrompt(*npc, menuTextConfig);
+		return hoverState;
+	}
+
+	Script *script = engine.getScript();
+	if (!script)
+		return hoverState;
+	hoverState.object = findRoomObjectAtPoint(engine, sceneObjects, mousePos);
+	hoverState.region = findRoomRegionAtPoint(engine, sceneRegions, mousePos);
+	if (entityManager && hoverState.object && hoverState.region) {
+		const int objectDrawIndex =
+			entityManager->findSceneEntityDrawIndexByName(hoverState.object->objectName);
+		const int regionDrawIndex =
+			entityManager->findSceneEntityDrawIndexByName(hoverState.region->regionName);
+		if (regionDrawIndex >= 0 && objectDrawIndex >= 0 && regionDrawIndex > objectDrawIndex)
+			hoverState.object = nullptr;
+	}
+	if (hoverState.object) {
+		hoverState.cursorSequence = resolveRoomObjectCursorSequence(*hoverState.object, *script);
+		hoverState.promptText = buildRoomObjectPrompt(
+			*hoverState.object, *script, hoverState.cursorSequence, menuTextConfig);
+		if (hoverState.cursorSequence != kCursorSequenceNeutral || !hoverState.promptText.empty())
+			return hoverState;
+		hoverState.object = nullptr;
+	}
+	if (hoverState.region && hoverState.region->cursorEnabled) {
+		hoverState.object = nullptr;
+		hoverState.cursorSequence = kCursorSequenceTransition;
+		hoverState.promptText.clear();
+		return hoverState;
+	}
+
+	if (Player::supportsMovementBand(state) &&
+		mousePos.y >= state.roomMaxZScreenY &&
+		mousePos.y <= state.roomMinZScreenY) {
+		hoverState.cursorSequence = kCursorSequenceWalk;
+	}
+
+	return hoverState;
+}
+
 Input::Input(HarvesterEngine &engine, Flow &flow, Scene &session, SceneState &scene,
 		InventorySystem &inventory, CheatSystem &cheats,
 		ItemInteractions &roomItems, Interaction &interactionProcessor,
diff --git a/engines/harvester/input.h b/engines/harvester/input.h
index f4eeddfcd52..224722a90d6 100644
--- a/engines/harvester/input.h
+++ b/engines/harvester/input.h
@@ -22,22 +22,30 @@
 #ifndef HARVESTER_INPUT_H
 #define HARVESTER_INPUT_H
 
+#include "common/array.h"
 #include "common/error.h"
 #include "common/events.h"
+#include "common/rect.h"
 
 namespace Harvester {
 
 class Art;
 class CheatSystem;
+class DialogueSystem;
 class Flow;
 class HarvesterEngine;
 class InventorySystem;
 class Interaction;
 class ItemInteractions;
 class Scene;
+struct MenuTextConfig;
+struct NpcRecord;
 struct ObjectRecord;
+struct RegionRecord;
 struct ResolvedText;
+struct RoomHoverState;
 struct RoomPresentationState;
+struct RoomSetupState;
 struct SceneState;
 
 struct InputState {
@@ -73,6 +81,15 @@ public:
 
 	InputResult handleEvent(const Common::Event &event, bool &needsRedraw);
 
+	static const ObjectRecord *findRoomObjectAtPoint(HarvesterEngine &engine,
+		const Common::Array<ObjectRecord> &sceneObjects, const Common::Point &point);
+	static RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupState &state,
+		const Common::Array<ObjectRecord> &sceneObjects,
+		const Common::Array<NpcRecord> &npcs,
+		const Common::Array<RegionRecord> &regions,
+		const Common::Point &mousePos, const MenuTextConfig &menuTextConfig,
+		const DialogueSystem *dialogue = nullptr);
+
 private:
 	void handleMouseMove(bool &needsRedraw);
 	InputResult handleRightButtonDown(bool &needsRedraw);
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index 35f6b22dddd..755d5895b66 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -71,16 +71,8 @@ void logScenePaletteSummary(const char *label, const SceneState &scene, float br
 bool captureScreenBackdrop(const Graphics::Screen &screen, IndexedBitmap &bitmap);
 bool shouldDisplaySceneAnimation(const AnimRecord &anim);
 bool shouldRunStartupRoomProbe();
-bool unlocksRoomObjectInteractionAfterInitialExamine(const ObjectRecord &object,
-	Script &script);
 bool doesPlayerFacingMatchRegion(int playerFacing, const RegionRecord &region);
 bool doesPlayerOverlapRegion(const Entity &playerEntity, const RegionRecord &region);
-RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupState &state,
-	const Common::Array<ObjectRecord> &sceneObjects,
-	const Common::Array<NpcRecord> &npcs,
-	const Common::Array<RegionRecord> &regions,
-	const Common::Point &mousePos, const MenuTextConfig &menuTextConfig,
-	const DialogueSystem *dialogue = nullptr);
 
 } // End of namespace Harvester
 


Commit: d092cf2688ceb23ae0bcc091b780e802a56c8384
    https://github.com/scummvm/scummvm/commit/d092cf2688ceb23ae0bcc091b780e802a56c8384
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-17T08:38:08-04:00

Commit Message:
HARVESTER: Fix cursor freezing after idle animations

Assisted-by: Codex:gpt-6.1-sol

Changed paths:
    engines/harvester/flow.cpp


diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 99a8e1073a7..cdca29d8b79 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -1552,7 +1552,9 @@ bool Flow::tickRuntimeEntities() {
 	EntityManager *entityManager = _engine.getRuntimeEntities();
 	if (!entityManager)
 		return false;
-	return entityManager->tickSceneEntities() || entityManager->syncCursorEntityPosition(_mousePos);
+	const bool sceneChanged = entityManager->tickSceneEntities();
+	const bool cursorChanged = entityManager->syncCursorEntityPosition(_mousePos);
+	return sceneChanged || cursorChanged;
 }
 
 bool Flow::handleSystemEvent(const Common::Event &event, Common::Error &result) {




More information about the Scummvm-git-logs mailing list