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

bluegr noreply at scummvm.org
Sat Sep 12 01:06:00 UTC 2026


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

Summary:
e9b24e62a1 NANCY: NANCY14: Fix color matching and winning in PaintPuzzle
f36970c8c3 NANCY: NANCY13-14: Fixes for secondary movies


Commit: e9b24e62a1f59c93c79ba118bf7fe842ce5355a7
    https://github.com/scummvm/scummvm/commit/e9b24e62a1f59c93c79ba118bf7fe842ce5355a7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-12T04:05:45+03:00

Commit Message:
NANCY: NANCY14: Fix color matching and winning in PaintPuzzle

- Read the solve scene info correctly
- Read sound data correctly
- Check against the correct color indices

Marchant's paintings can now be solved correctly

Changed paths:
    engines/nancy/action/puzzle/paintpuzzle.cpp
    engines/nancy/action/puzzle/paintpuzzle.h


diff --git a/engines/nancy/action/puzzle/paintpuzzle.cpp b/engines/nancy/action/puzzle/paintpuzzle.cpp
index f39d5632900..326362ab368 100644
--- a/engines/nancy/action/puzzle/paintpuzzle.cpp
+++ b/engines/nancy/action/puzzle/paintpuzzle.cpp
@@ -19,11 +19,14 @@
  *
  */
 
+#include "common/random.h"
+
 #include "engines/nancy/nancy.h"
 #include "engines/nancy/cursor.h"
 #include "engines/nancy/graphics.h"
 #include "engines/nancy/input.h"
 #include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
 #include "engines/nancy/util.h"
 
 #include "engines/nancy/action/puzzle/paintpuzzle.h"
@@ -64,12 +67,15 @@ void PaintPuzzle::readData(Common::SeekableReadStream &stream) {
 	_sounds[0].readData(stream);	// 0xa4
 	_sounds[1].readData(stream);	// 0xfa
 
-	_field1a6 = stream.readSint16LE();		// 0x1a6
-	_outcome.field0 = stream.readSint16LE();	// 0x1a8
-	_outcome.sceneID = stream.readSint16LE();
-	_outcome.flag = stream.readByte();
+	// Shorter than the SceneChangeWithFlag::readData() formats: no vertical
+	// offset or scene sound field
+	_solveScene._sceneChange.sceneID = stream.readUint16LE();	// 0x1a6
+	_solveScene._sceneChange.frameID = stream.readUint16LE();
+	_solveScene._sceneChange.continueSceneSound = kContinueSceneSound;
+	_solveScene._flag.label = stream.readSint16LE();
+	_solveScene._flag.flag = stream.readByte();
 
-	_sounds[2].readData(stream);
+	_solveSound.readData(stream);			// 0x150
 
 	// Trailing count-prefixed array of 23-byte give-up hotspots
 	// {Rect, uint16 cursorType, uint16 sceneID, int16 flagLabel, byte flagValue}.
@@ -101,7 +107,6 @@ void PaintPuzzle::init() {
 	_hoverRegion = -1;
 	_hoverColor = -1;
 	_solved = false;
-	_outcomeApplied = false;
 
 	redraw();
 }
@@ -152,7 +157,7 @@ int PaintPuzzle::regionAtCursor(const Common::Point &mousePos) const {
 // Draws a painted region: its overlay shape recolored to the flat fill color.
 void PaintPuzzle::drawRegion(uint regionIndex) {
 	const PaintRegion &region = _regions[regionIndex];
-	int c = region.currentColor;
+	int c = region.currentColor - 1;
 	if (c < 0 || c >= (int)_colors.size() || regionIndex >= _regionImages.size()) {
 		return;
 	}
@@ -242,7 +247,7 @@ void PaintPuzzle::redraw() {
 	// Only painted regions are drawn on the overlay; the picture outline and
 	// palette come from the scene background.
 	for (uint i = 0; i < _regions.size(); ++i) {
-		if (_regions[i].currentColor >= 0) {
+		if (_regions[i].currentColor > 0) {
 			drawRegion(i);
 		}
 	}
@@ -265,18 +270,37 @@ bool PaintPuzzle::isSolved() const {
 }
 
 void PaintPuzzle::paintRegion(uint regionIndex, int colorIndex) {
-	_regions[regionIndex].currentColor = (int16)colorIndex;
+	// Region colors are 1-based palette indices; 0 means unpainted
+	_regions[regionIndex].currentColor = (int16)(colorIndex + 1);
 	if (isSolved()) {
 		_solved = true;
 	}
 	redraw();
 }
 
-void PaintPuzzle::applyOutcome(const SceneOutcome &outcome) {
-	SceneChangeDescription desc;
-	desc.sceneID = outcome.sceneID;
-	NancySceneState.changeScene(desc);
-	NancySceneState.setEventFlag(outcome.field0, outcome.flag);
+void PaintPuzzle::playSoundBlock(const RandomSoundBlock &block) {
+	if (block.names.empty()) {
+		return;
+	}
+
+	uint idx = block.names.size() == 1 ? 0 : g_nancy->_randomSource->getRandomNumber(block.names.size() - 1);
+	const Common::String &name = block.names[idx];
+	if (name.empty() || name == "NO SOUND") {
+		return;
+	}
+
+	SoundDescription desc;
+	desc.name = name;
+	desc.channelID = block.channel;
+	desc.numLoops = block.numLoops > 0 ? block.numLoops : 1;
+	desc.volume = block.volume;
+
+	g_nancy->_sound->loadSound(desc);
+	g_nancy->_sound->playSound(desc);
+}
+
+bool PaintPuzzle::isSoundBlockPlaying(const RandomSoundBlock &block) const {
+	return !block.names.empty() && g_nancy->_sound->isSoundPlaying((uint16)block.channel);
 }
 
 void PaintPuzzle::handleInput(NancyInput &input) {
@@ -338,15 +362,27 @@ void PaintPuzzle::execute() {
 		_state = kRun;
 		break;
 	case kRun:
+		if (_exitRequested || _solved) {
+			if (_solved) {
+				playSoundBlock(_solveSound);
+			}
+			_state = kActionTrigger;
+		}
+		break;
+	case kActionTrigger:
+		// The solve sound gets to finish first
+		if (!_exitRequested && isSoundBlockPlaying(_solveSound)) {
+			break;
+		}
+
 		if (_exitRequested) {
 			NancySceneState.setEventFlag(_exitFlag);
 			NancySceneState.changeScene(_exitScene);
-			break;
-		}
-		if (_solved && !_outcomeApplied) {
-			_outcomeApplied = true;
-			applyOutcome(_outcome);
+		} else {
+			_solveScene.execute();
 		}
+
+		finishExecution();
 		break;
 	default:
 		break;
diff --git a/engines/nancy/action/puzzle/paintpuzzle.h b/engines/nancy/action/puzzle/paintpuzzle.h
index 0cce3fd9f17..fbe7ad9c093 100644
--- a/engines/nancy/action/puzzle/paintpuzzle.h
+++ b/engines/nancy/action/puzzle/paintpuzzle.h
@@ -58,7 +58,8 @@ protected:
 	};
 
 	// A fillable region of the picture: an overlay shape drawn at a position,
-	// its current color index, and the target it must hold to be solved.
+	// its current color, and the target it must hold to be solved. Colors are
+	// 1-based palette indices; 0 means unpainted.
 	struct PaintRegion {
 		Common::Path name;
 		Common::Rect rect;
@@ -66,12 +67,6 @@ protected:
 		int16 targetColor = -1;
 	};
 
-	struct SceneOutcome {
-		int16 field0 = 0;
-		int16 sceneID = 0;
-		byte flag = 0;
-	};
-
 	int colorSwatchAtCursor(const Common::Point &mousePos) const;
 	int regionAtCursor(const Common::Point &mousePos) const;
 	// Alpha of the region overlay's shape at (x,y): the pixel's alpha channel, or
@@ -82,7 +77,8 @@ protected:
 	void drawBrush();
 	void redraw();
 	bool isSolved() const;
-	void applyOutcome(const SceneOutcome &outcome);
+	void playSoundBlock(const RandomSoundBlock &block);
+	bool isSoundBlockPlaying(const RandomSoundBlock &block) const;
 
 	// -- File data --
 	Common::Path _imageName;		// 0x3d
@@ -93,10 +89,10 @@ protected:
 	Common::Array<PaintColor> _colors;		// 0x78
 	Common::Array<PaintRegion> _regions;	// 0x94
 
-	RandomSoundBlock _sounds[3];	// 0xa4/0xfa (before) + one after the outcome
+	RandomSoundBlock _sounds[2];	// 0xa4/0xfa
 
-	int16 _field1a6 = 0;		// 0x1a6
-	SceneOutcome _outcome;		// 0x1a8
+	SceneChangeWithFlag _solveScene;	// 0x1a6
+	RandomSoundBlock _solveSound;		// 0x150, plays before the solve scene change
 
 	// Give-up hotspot (count-prefixed 23-byte trailer): click to leave the puzzle.
 	Common::Rect _exitHotspot;
@@ -112,7 +108,6 @@ protected:
 	int _hoverRegion = -1;
 	int _hoverColor = -1;
 	bool _solved = false;
-	bool _outcomeApplied = false;
 	bool _exitRequested = false;
 };
 


Commit: f36970c8c32acbc4fb7ec07d5df7ce61483e92e5
    https://github.com/scummvm/scummvm/commit/f36970c8c32acbc4fb7ec07d5df7ce61483e92e5
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-12T04:05:46+03:00

Commit Message:
NANCY: NANCY13-14: Fixes for secondary movies

- Fixes talking to some characters in Nancy14 (e.g. Dieter and Monique)
- Add handling for random pauses in turning animations

Changed paths:
    engines/nancy/action/secondarymovie.cpp
    engines/nancy/action/secondarymovie.h


diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index 2c78e7a7159..7dd98d84934 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -130,17 +130,20 @@ void PlaySecondaryMovie::handleInput(NancyInput &input) {
 		return;
 	}
 
-	// The character's box (set as the hotspot while it is on screen) is
-	// clickable; clicking opens its conversation scene, and hovering drives the
-	// recognition movie. The talk hover cursor is applied by ActionManager via
-	// getHoverCursor().
-	if (!_hasHotspot || _talkSceneID == kNoScene) {
-		_isHovered = false;
+	// The mouse is hit-tested against the movie's own box whether or not the
+	// record is clickable, since the sequence chain reacts to hovering on its
+	// own (Nancy14 characters turn toward the player that way, Nancy13 ones
+	// play their recognition movie).
+	//
+	// A character that names a conversation scene is clickable on top of that;
+	// its hover cursor is applied by ActionManager via getHoverCursor().
+	_isHovered = _isVisible &&
+		NancySceneState.getViewport().convertViewportToScreen(_screenPosition).contains(input.mousePos);
+
+	if (_talkSceneID == kNoScene) {
 		return;
 	}
 
-	_isHovered = NancySceneState.getViewport().convertViewportToScreen(_hotspot).contains(input.mousePos);
-
 	if (_isHovered && (input.input & NancyInput::kLeftMouseButtonUp)) {
 		input.eatMouseInput();
 		SceneChangeDescription desc;
@@ -150,9 +153,10 @@ void PlaySecondaryMovie::handleInput(NancyInput &input) {
 }
 
 CursorManager::CursorType PlaySecondaryMovie::getHoverCursor() const {
-	// The character's own cursor type (a raw Nancy13 cursor id) comes from the
-	// secondary record; cursorSetFromScript() routes it through the raw-slot path.
-	return (CursorManager::CursorType)_talkCursorType;
+	// The character's own cursor type (a raw cursor id) comes from the chunk;
+	// cursorSetFromScript() routes it through the raw-slot path. Records that
+	// don't name one keep the generic hotspot cursor.
+	return _talkCursorType >= 0 ? (CursorManager::CursorType)_talkCursorType : CursorManager::kHotspot;
 }
 
 void PlaySecondaryMovie::readRandomSequence(Common::Serializer &ser, RandomSequence &seq) {
@@ -177,8 +181,35 @@ void PlaySecondaryMovie::readRandomSequence(Common::Serializer &ser, RandomSeque
 
 	seq.nextSequences.resize(nextCount);
 	for (uint i = 0; i < nextCount; ++i) {
-		readFilename(ser, seq.nextSequences[i].name);
-		ser.syncAsUint16LE(seq.nextSequences[i].weight);
+		NextSequenceRef &next = seq.nextSequences[i];
+		readFilename(ser, next.name);
+
+		// A negative weight isn't a weight at all, but one of the special flags
+		// that make this entry the one picked when its condition holds.
+		int16 weight = 0;
+		ser.syncAsSint16LE(weight);
+
+		switch (weight) {
+		case -1:
+			next.condition = kNextEqualChance;
+			seq.equalChanceNext = true;
+			break;
+		case -2:
+			next.condition = kNextIfHovered;
+			break;
+		case -3:
+			next.condition = kNextIfNotHovered;
+			break;
+		case -4:
+			next.condition = kNextIfChannel13Playing;
+			break;
+		case -5:
+			next.condition = kNextIfChannel12Playing;
+			break;
+		default:
+			next.weight = weight;
+			break;
+		}
 	}
 }
 
@@ -192,7 +223,7 @@ void PlaySecondaryMovie::readSecondaryRandomMovie(Common::Serializer &ser, Rando
 	readFilename(ser, seq.name);
 	ser.syncAsUint16LE(seq.startFrame);
 	ser.syncAsUint16LE(seq.lastFrame);
-	ser.syncAsUint16LE(_talkCursorType);	// hover cursor for the character
+	ser.syncAsSint16LE(_talkCursorType);	// hover cursor for the character
 	ser.syncAsUint16LE(_talkSceneID);
 	ser.skip(2);	// conversation frameID (0 in known data)
 
@@ -255,9 +286,13 @@ void PlaySecondaryMovie::readRandomMovieDataNancy14(Common::Serializer &ser, Com
 	_videoFormat = kLargeVideoFormat;
 	ser.skip(2);	// Visibility frame ID; ScummVM drives visibility from the videoDescs
 	ser.syncAsUint16LE(_randomPlayerCursorAllowed);
-	ser.skip(4);	// Two u16s (object offsets 0x8c / 0xe7); purpose not yet mapped
-	ser.syncAsSint16LE(_sceneChange.sceneID);
-	ser.syncAsUint16LE(_sceneChange.frameID);
+	ser.skip(2);	// Event flag gating the roll for a next sequence; -1 = always roll
+	// Talkable character: the cursor shown while the mouse is over it, and the
+	// conversation scene a click opens. -1 / kNoScene mean the record carries
+	// neither, i.e. the character isn't clickable.
+	ser.syncAsSint16LE(_talkCursorType);
+	ser.syncAsUint16LE(_talkSceneID);
+	ser.skip(2);	// Conversation frame ID (0 in known data)
 
 	ser.syncAsByte(_movieVolume);
 	_movieVolume = MIN<byte>(_movieVolume, 100);
@@ -447,18 +482,41 @@ void PlaySecondaryMovie::playRandomSequence() {
 }
 
 int PlaySecondaryMovie::beginRandomPause(const RandomSequence &seq) {
+	_randomChainState = kRandomPaused;
+
+	// Two of the pause values are sentinels: instead of a duration they hold
+	// the sequence on its last frame until the mouse enters or leaves the
+	// movie, which is how a character keeps looking at the player for as long
+	// as the mouse stays on them.
+	if (seq.minPauseMs == -2 || seq.minPauseMs == -3) {
+		_randomPauseMode = seq.minPauseMs == -2 ? kPauseUntilHovered : kPauseUntilNotHovered;
+		return -1;
+	}
+
 	int32 pauseMs = seq.minPauseMs;
 	if (seq.maxPauseMs > seq.minPauseMs) {
 		pauseMs += g_nancy->_randomSource->getRandomNumber(seq.maxPauseMs - seq.minPauseMs - 1);
 	}
+
+	_randomPauseMode = kPauseTimed;
 	_randomPauseEndTime = g_system->getMillis() + (uint32)MAX<int32>(0, pauseMs);
-	_randomChainState = kRandomPaused;
 	setVisible(false);
 	_mask.setVisible(false);
 	_decoder.pauseVideo(true);
 	return -1;
 }
 
+bool PlaySecondaryMovie::randomPauseElapsed() const {
+	switch (_randomPauseMode) {
+	case kPauseUntilHovered:
+		return _isHovered;
+	case kPauseUntilNotHovered:
+		return !_isHovered;
+	default:
+		return g_system->getMillis() >= _randomPauseEndTime;
+	}
+}
+
 int PlaySecondaryMovie::lookupSequence(const Common::Path &name) const {
 	for (uint j = 0; j < _sequences.size(); ++j) {
 		if (_sequences[j].name == name) {
@@ -469,45 +527,93 @@ int PlaySecondaryMovie::lookupSequence(const Common::Path &name) const {
 	return -1;
 }
 
-int PlaySecondaryMovie::rollNextSequence() {
+int PlaySecondaryMovie::pickNextSequence() {
 	if (_activeSequenceIndex < 0 || _activeSequenceIndex >= (int)_sequences.size()) {
 		return -1;
 	}
 
 	const RandomSequence &seq = _sequences[_activeSequenceIndex];
 
-	if (g_nancy->getGameType() >= kGameTypeNancy13) {
-		// Two independent rolls: first a percent chance to stay on this
-		// sequence and pause, then a percent-weighted pick among the next
-		// sequences (weights sum to 100, or all EQUAL_CHANCE for a uniform pick).
-		if (seq.stayWeight != 0 && (uint)g_nancy->_randomSource->getRandomNumber(99) < seq.stayWeight) {
-			return beginRandomPause(seq);
+	if (seq.nextSequences.empty()) {
+		_randomChainState = kRandomPaused;
+		_randomPauseMode = kPauseTimed;
+		_randomPauseEndTime = g_system->getMillis() + 1000;	// re-check in 1s
+		return -1;
+	}
+
+	// The special-flag entries are tried first, in the order the original uses:
+	// the one matching the current hover state, then the sound-gated ones.
+	const NextCondition hoverCondition = _isHovered ? kNextIfHovered : kNextIfNotHovered;
+	for (const NextSequenceRef &next : seq.nextSequences) {
+		if (next.condition == hoverCondition) {
+			return lookupSequence(next.name);
 		}
 
-		if (seq.nextSequences.empty()) {
-			_randomChainState = kRandomPaused;
-			_randomPauseEndTime = g_system->getMillis() + 1000;	// re-check in 1s
-			return -1;
+		if (next.condition == kNextIfChannel12Playing || next.condition == kNextIfChannel13Playing) {
+			warning("PlayRandomMovie: sound-gated next-sequence \"%s\" is not implemented",
+				next.name.toString().c_str());
 		}
+	}
 
-		const bool equalChance = seq.nextSequences[0].weight == 0xFFFF;
-		const uint step = 100 / seq.nextSequences.size();
-		uint roll = g_nancy->_randomSource->getRandomNumber(99);
-		uint cumulative = 0;
-		for (uint i = 0; i < seq.nextSequences.size(); ++i) {
-			if (i == seq.nextSequences.size() - 1) {
-				cumulative = 100;
-			} else {
-				cumulative += equalChance ? step : seq.nextSequences[i].weight;
-			}
-			if (roll < cumulative) {
-				return lookupSequence(seq.nextSequences[i].name);
-			}
+	// Otherwise a percent-weighted pick among the weighted entries, whose
+	// weights sum to 100 (or take an equal share each).
+	uint numWeighted = 0;
+	for (const NextSequenceRef &next : seq.nextSequences) {
+		if (next.condition == kNextWeighted || next.condition == kNextEqualChance) {
+			++numWeighted;
 		}
+	}
 
+	if (numWeighted == 0) {
+		// Nothing but conditions that don't hold right now. Hold the sequence
+		// on its last frame and re-check shortly, since moving the mouse can
+		// change the answer.
+		_randomChainState = kRandomPaused;
+		_randomPauseMode = kPauseTimed;
+		_randomPauseEndTime = g_system->getMillis() + 100;
 		return -1;
 	}
 
+	const uint step = 100 / numWeighted;
+	const uint roll = g_nancy->_randomSource->getRandomNumber(99);
+	uint cumulative = 0;
+	uint weightedIndex = 0;
+	for (const NextSequenceRef &next : seq.nextSequences) {
+		if (next.condition != kNextWeighted && next.condition != kNextEqualChance) {
+			continue;
+		}
+
+		if (++weightedIndex == numWeighted) {
+			cumulative = 100;
+		} else {
+			cumulative += seq.equalChanceNext ? step : next.weight;
+		}
+
+		if (roll < cumulative) {
+			return lookupSequence(next.name);
+		}
+	}
+
+	return -1;
+}
+
+int PlaySecondaryMovie::rollNextSequence() {
+	if (_activeSequenceIndex < 0 || _activeSequenceIndex >= (int)_sequences.size()) {
+		return -1;
+	}
+
+	const RandomSequence &seq = _sequences[_activeSequenceIndex];
+
+	if (g_nancy->getGameType() >= kGameTypeNancy13) {
+		// First a percent chance to stay on this sequence and pause; the pick
+		// among the next sequences happens once the pause is over.
+		if (seq.stayWeight != 0 && (uint)g_nancy->_randomSource->getRandomNumber(99) < seq.stayWeight) {
+			return beginRandomPause(seq);
+		}
+
+		return pickNextSequence();
+	}
+
 	uint32 totalWeight = seq.stayWeight;
 	for (const NextSequenceRef &ns : seq.nextSequences) {
 		totalWeight += ns.weight;
@@ -940,11 +1046,11 @@ void PlaySecondaryMovie::execute() {
 				_state = kActionTrigger;
 				break;
 			}
-			if (g_system->getMillis() < _randomPauseEndTime) {
+			if (!randomPauseElapsed()) {
 				break;
 			}
 			_randomChainState = kRandomPlaying;
-			int picked = rollNextSequence();
+			int picked = g_nancy->getGameType() >= kGameTypeNancy13 ? pickNextSequence() : rollNextSequence();
 			if (picked >= 0) {
 				activateRandomSequence(picked);
 			}
diff --git a/engines/nancy/action/secondarymovie.h b/engines/nancy/action/secondarymovie.h
index d9cb365d71b..f78dff85ad2 100644
--- a/engines/nancy/action/secondarymovie.h
+++ b/engines/nancy/action/secondarymovie.h
@@ -60,11 +60,25 @@ public:
 		FlagDescription flagDesc;
 	};
 
+	// What makes a next-sequence entry the one picked once the current sequence
+	// finishes. Entries carry a percent weight, unless the chunk tags them with
+	// one of the negative "special flag" values below; a tagged entry is picked
+	// whenever its condition holds, ahead of the weighted roll.
+	enum NextCondition {
+		kNextWeighted		= 0,	// ordinary percent weight
+		kNextEqualChance,			// -1: uniform share among the entries
+		kNextIfHovered,				// -2: the mouse is over the movie
+		kNextIfNotHovered,			// -3: it isn't
+		kNextIfChannel13Playing,	// -4
+		kNextIfChannel12Playing		// -5
+	};
+
 	// Name of the next sequence to chain to once the current one finishes,
-	// plus its selection weight in the weighted random pick.
+	// plus what makes it the one picked.
 	struct NextSequenceRef {
 		Common::Path name;
 		uint16 weight = 0;
+		NextCondition condition = kNextWeighted;
 	};
 
 	// `name` is both the sequence id and the movie filename.
@@ -76,9 +90,11 @@ public:
 		int32 maxPauseMs = 0;
 		// Weight assigned to "stay on this sequence" in the weighted random
 		// pick. A roll inside [0, stayWeight) means "don't transition";
-		// instead pause for [minPauseMs, maxPauseMs] and re-roll.
+		// instead pause for [minPauseMs, maxPauseMs] before moving on.
 		uint16 stayWeight = 0;
 		Common::Array<NextSequenceRef> nextSequences;
+		// Every weighted entry takes an equal share of the pick.
+		bool equalChanceNext = false;
 	};
 
 	// Which of the action record types sharing this class is being played.
@@ -189,24 +205,31 @@ public:
 	Common::Path _maskName;
 	Common::Array<SecondaryVideoDescription> _maskDescs;
 
-	// Nancy13 talkable characters: the scene to open when the character is
+	// Talkable characters (Nancy13+): the scene to open when the character is
 	// clicked (its conversation). kNoScene means the character isn't clickable.
 	uint16 _talkSceneID = kNoScene;
-	// Hover cursor for the character (a raw Nancy13 cursor id from the chunk).
-	uint16 _talkCursorType = 0;
+	// Hover cursor for the character (a raw cursor id from the chunk), or -1
+	// when the record doesn't name one.
+	int16 _talkCursorType = -1;
 
 	// Chain state. After a sequence's movie finishes the engine rolls a
 	// weighted pick: "stay" -> enter pause for a random duration and
 	// re-roll; valid next-sequence -> swap to that sequence's movie.
 	enum RandomChainState { kRandomPlaying, kRandomPaused };
+	// What ends the pause: its duration running out, or the mouse entering or
+	// leaving the movie (minPauseMs -2 / -3). A sequence waiting on the mouse
+	// holds its last frame on screen instead of hiding.
+	enum RandomPauseMode { kPauseTimed, kPauseUntilHovered, kPauseUntilNotHovered };
 	int _activeSequenceIndex = -1;
 	RandomChainState _randomChainState = kRandomPlaying;
+	RandomPauseMode _randomPauseMode = kPauseTimed;
 	uint32 _randomPauseEndTime = 0;
 	bool _randomStopRequested = false;
 	bool _randomPaused = false;
 
-	// Talkable-character hover state: whether the mouse is over the character,
-	// and whether the recognition (secondary) movie is currently playing.
+	// Whether the mouse is over the movie (which drives both the hover-based
+	// sequence chain and the click that opens a character's conversation), and
+	// whether the recognition (secondary) movie is currently playing.
 	bool _isHovered = false;
 	bool _playingSecondary = false;
 
@@ -240,7 +263,7 @@ public:
 	// hovering plays the recognition ("turn around") movie.
 	void handleInput(NancyInput &input) override;
 	CursorManager::CursorType getHoverCursor() const override;
-	bool cursorSetFromScript() const override { return isRandom() && _talkSceneID != kNoScene; }
+	bool cursorSetFromScript() const override { return isRandom() && _talkSceneID != kNoScene && _talkCursorType >= 0; }
 
 	Common::String getRecordExtraInfo() const override {
 		return Common::String::format("Scene %d, file %s", _sceneChange.sceneID, _videoName.baseName().c_str());
@@ -307,10 +330,19 @@ protected:
 	// or the chosen sequence index otherwise.
 	int rollNextSequence();
 
+	// Pick the sequence to chain to, without rolling for "stay" first: the
+	// special-flag entries take priority over the weighted random pick.
+	// Returns the chosen sequence index, or -1 if nothing was picked.
+	int pickNextSequence();
+
 	// Enter the paused chain state for a random duration in the sequence's
-	// [minPauseMs, maxPauseMs] range. Always returns -1.
+	// [minPauseMs, maxPauseMs] range, or until the mouse enters or leaves the
+	// movie. Always returns -1.
 	int beginRandomPause(const RandomSequence &seq);
 
+	// Whether whatever the current pause is waiting for has happened.
+	bool randomPauseElapsed() const;
+
 	// Find a sequence by name, warning and returning -1 if it isn't present.
 	int lookupSequence(const Common::Path &name) const;
 




More information about the Scummvm-git-logs mailing list