[Scummvm-git-logs] scummvm master -> 9c143cbe8d45bf93c7e8d659925031d601dddbc3

bluegr noreply at scummvm.org
Sun Jul 26 16:29:35 UTC 2026


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

Summary:
c33e2d8f3b NANCY: NANCY3: Properly handle hotspots for animated overlays
a0f6e463bb NANCY: Overlay dimensions are calculated from their source rect
d759a711e6 NANCY: NANCY12: Implement new timer design
3aacdcadaa NANCY: NANCY12: Add new transparency mode
9c143cbe8d NANCY: NANCY10: Bottom-align checkboxes in the task list


Commit: c33e2d8f3b7674f554285286f80b3614605921cf
    https://github.com/scummvm/scummvm/commit/c33e2d8f3b7674f554285286f80b3614605921cf
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T19:29:21+03:00

Commit Message:
NANCY: NANCY3: Properly handle hotspots for animated overlays

This mirrors the handling does for static overlays, as _enableHotspot
is only used in Nancy2.

Fixes giving the toy mouse to Uri the cat in Nancy12

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


diff --git a/engines/nancy/action/overlay.cpp b/engines/nancy/action/overlay.cpp
index 55a6da14b21..7923a076415 100644
--- a/engines/nancy/action/overlay.cpp
+++ b/engines/nancy/action/overlay.cpp
@@ -103,7 +103,7 @@ void Overlay::readData(Common::SeekableReadStream &stream) {
 	ser.skip(2); // VIDEO_STOP_RENDERING or VIDEO_CONTINUE_RENDERING
 	ser.syncAsUint16LE(_transparency);
 	ser.syncAsUint16LE(_hasSceneChange);
-	ser.syncAsUint16LE(_enableHotspot, kGameTypeNancy2, kGameTypeNancy2);
+	ser.syncAsUint16LE(_enableHotspotNancy2, kGameTypeNancy2, kGameTypeNancy2);
 	ser.syncAsUint16LE(_z, kGameTypeNancy2);
 	ser.syncAsUint16LE(_overlayType, kGameTypeNancy2);
 	ser.syncAsUint16LE(numSrcRects, kGameTypeNancy2);
@@ -122,12 +122,6 @@ void Overlay::readData(Common::SeekableReadStream &stream) {
 
 	ser.syncAsUint16LE(_z, kGameTypeNancy1, kGameTypeNancy1);
 
-	if (ser.getVersion() > kGameTypeNancy2) {
-		if (_overlayType == kPlayOverlayStatic) {
-			_enableHotspot = (_hasSceneChange == kPlayOverlaySceneChange) ? kPlayOverlayWithHotspot : kPlayOverlayNoHotspot;
-		}
-	}
-
 	if (_isInterruptible) {
 			ser.syncAsSint16LE(_interruptCondition.label);
 			ser.syncAsUint16LE(_interruptCondition.flag);
@@ -201,9 +195,16 @@ void Overlay::execute() {
 							moveTo(_blitDescriptions[i].dest);
 							setVisible(true);
 
-							if (_enableHotspot == kPlayOverlayWithHotspot) {
-								_hotspot = _screenPosition;
-								_hasHotspot = true;
+							if (g_nancy->getGameType() <= kGameTypeNancy2) {
+								if (_enableHotspotNancy2 == kPlayOverlayWithHotspot) {
+									_hotspot = _screenPosition;
+									_hasHotspot = true;
+								}
+							} else {
+								if (_blitDescriptions[i].hasHotspot == kPlayOverlayWithHotspot) {
+									_hotspot = _screenPosition;
+									_hasHotspot = true;
+								}
 							}
 
 							break;
@@ -340,7 +341,7 @@ void Overlay::execute() {
 
 						if (g_nancy->getGameType() <= kGameTypeNancy2) {
 							// In nancy2, the presence of a hotspot relies on whether the Overlay has a scene change
-							if (_enableHotspot == kPlayOverlayWithHotspot) {
+							if (_enableHotspotNancy2 == kPlayOverlayWithHotspot) {
 								_hotspot = _screenPosition;
 								_hasHotspot = true;
 							}
diff --git a/engines/nancy/action/overlay.h b/engines/nancy/action/overlay.h
index 888f666ab1d..25535db74cf 100644
--- a/engines/nancy/action/overlay.h
+++ b/engines/nancy/action/overlay.h
@@ -57,7 +57,7 @@ public:
 
 	uint16 _transparency = kPlayOverlayPlain;
 	uint16 _hasSceneChange = kPlayOverlaySceneChange;
-	uint16 _enableHotspot = kPlayOverlayNoHotspot;
+	uint16 _enableHotspotNancy2 = kPlayOverlayNoHotspot;
 	uint16 _overlayType = kPlayOverlayAnimated;
 	uint16 _playDirection = kPlayOverlayForward;
 	uint16 _loop = kPlayOverlayOnce;
@@ -85,6 +85,7 @@ public:
 
 	bool canHaveHotspot() const override { return true; }
 	bool isViewportRelative() const override { return true; }
+	Common::String getRecordExtraInfo() const override { return Common::String::format("Scene %d", _sceneChange.sceneID); }
 
 protected:
 	Common::String getRecordTypeName() const override;


Commit: a0f6e463bb2dde5a92903eaf01c2d4d4049533ed
    https://github.com/scummvm/scummvm/commit/a0f6e463bb2dde5a92903eaf01c2d4d4049533ed
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T19:29:23+03:00

Commit Message:
NANCY: Overlay dimensions are calculated from their source rect

Now, the axe and logs in Nancy10 scene 2951 are rendered correctly

Fix #16977

Changed paths:
    engines/nancy/action/overlay.cpp


diff --git a/engines/nancy/action/overlay.cpp b/engines/nancy/action/overlay.cpp
index 7923a076415..1b165274393 100644
--- a/engines/nancy/action/overlay.cpp
+++ b/engines/nancy/action/overlay.cpp
@@ -403,9 +403,15 @@ void OverlayStaticTerse::readData(Common::SeekableReadStream &stream) {
 	readRect(stream, dest);
 	readRect(stream, src);
 
-	_srcRects.push_back(src);
+	// The source rect only supplies the top-left offset into the image; the overlay
+	// is blitted 1:1, so the source region takes the destination's dimensions. Using
+	// the source rect's own (smaller) size here would scale the image to fit the destination.
+	Common::Rect srcRect(dest.width(), dest.height());
+	srcRect.moveTo(src.left, src.top);
+
+	_srcRects.push_back(srcRect);
 	_blitDescriptions.resize(1);
-	_blitDescriptions[0].src = Common::Rect(src.width(), src.height());
+	_blitDescriptions[0].src = Common::Rect(dest.width(), dest.height());
 	_blitDescriptions[0].dest = dest;
 
 	_overlayType = kPlayOverlayStatic;


Commit: d759a711e6b002bfb0f4a465ca40f5d7a67ff7ec
    https://github.com/scummvm/scummvm/commit/d759a711e6b002bfb0f4a465ca40f5d7a67ff7ec
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T19:29:24+03:00

Commit Message:
NANCY: NANCY12: Implement new timer design

Fixes giving the mouse to Uri the cat

Changed paths:
    engines/nancy/action/miscrecords.cpp
    engines/nancy/action/miscrecords.h
    engines/nancy/nancy.h
    engines/nancy/puzzledata.cpp
    engines/nancy/puzzledata.h
    engines/nancy/state/scene.cpp
    engines/nancy/state/scene.h


diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index 4aac737c170..614f32f7bbb 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -34,6 +34,7 @@
 
 #include "common/events.h"
 #include "common/config-manager.h"
+#include "common/random.h"
 #include "nancy/ui/taskbar.h"
 
 namespace Nancy {
@@ -439,19 +440,175 @@ static TimerData::Timer *getSoftwareTimer(int16 index) {
 	return timerData ? &timerData->timers[index] : nullptr;
 }
 
+// Nancy 12+: adds a trigger to a running timer, unless an identical one already
+// exists (so re-running the record on scene re-entry doesn't stack duplicates)
+// or the timer's kNumTriggers slots are full.
+static void addTimerTrigger(TimerData::Timer &timer, TimerData::Trigger::Type type,
+		uint32 durationMs, const SoundDescription &sound, const Common::Array<FlagDescription> &flags) {
+	for (uint i = 0; i < timer.triggers.size(); ++i) {
+		const TimerData::Trigger &t = timer.triggers[i];
+		if (t.type != type || t.durationMs != durationMs ||
+			t.sound.name != sound.name || t.sound.channelID != sound.channelID ||
+			t.sound.volume != sound.volume) {
+			continue;
+		}
+
+		bool flagsMatch = true;
+		for (uint j = 0; j < ARRAYSIZE(t.flags); ++j) {
+			FlagDescription incoming = j < flags.size() ? flags[j] : FlagDescription();
+			if (t.flags[j].label != incoming.label || t.flags[j].flag != incoming.flag) {
+				flagsMatch = false;
+				break;
+			}
+		}
+
+		if (flagsMatch) {
+			return;
+		}
+	}
+
+	if (timer.triggers.size() >= TimerData::kNumTriggers) {
+		return;
+	}
+
+	TimerData::Trigger trigger;
+	trigger.type = type;
+	trigger.durationMs = durationMs;
+	trigger.hasFired = false;
+	trigger.sound = sound;
+	for (uint i = 0; i < ARRAYSIZE(trigger.flags); ++i) {
+		trigger.flags[i] = i < flags.size() ? flags[i] : FlagDescription();
+	}
+
+	timer.triggers.push_back(trigger);
+}
+
+// Reads exactly size bytes from the stream, returning the text up to the first
+// null byte. Used for the fixed-size string fields inside a TimerControl chunk.
+static Common::String readFixedSizeString(Common::SeekableReadStream &stream, uint size) {
+	Common::String result;
+	bool ended = false;
+	for (uint i = 0; i < size; ++i) {
+		byte b = stream.readByte();
+		if (b == 0) {
+			ended = true;
+		}
+		if (!ended) {
+			result += (char)b;
+		}
+	}
+
+	return result;
+}
+
 void ResetAndStartTimer::readData(Common::SeekableReadStream &stream) {
-	_timerIndex = stream.readByte();
+	if (g_nancy->getGameType() < kGameTypeNancy12) {
+		_timerIndex = stream.readByte();
+		return;
+	}
+
+	_timerIndex = stream.readSint16LE();    // 0x00
+	_command = stream.readSint16LE();        // 0x02
+
+	switch (_command) {
+	case kAddTime:
+	case kSubtractTime:
+	case kSetTime:
+		_hours = stream.readSint16LE();      // 0x04
+		_minutes = stream.readSint16LE();    // 0x06
+		_seconds = stream.readSint16LE();    // 0x08
+		stream.skip(2);                      // 0x0a, unused
+		break;
+	case kConfigOneShot:
+	case kConfigRepeating: {
+		_hours = stream.readSint16LE();          // 0x04
+		_minutes = stream.readSint16LE();        // 0x06
+		_seconds = stream.readSint16LE();        // 0x08
+		stream.skip(2);                          // 0x0a, unused
+		_sound.volume = stream.readUint16LE();   // 0x0c
+		_sound.channelID = stream.readUint16LE(); // 0x0e
+		_sound.numLoops = 1;
+
+		// Three candidate expiry-sound names (0x10, 33 bytes each); one is
+		// picked at random, "NO SOUND" marks an empty slot
+		Common::Array<Common::String> names;
+		for (uint i = 0; i < 3; ++i) {
+			Common::String name = readFixedSizeString(stream, 0x21);
+			if (!name.empty() && !name.equalsIgnoreCase("NO SOUND")) {
+				names.push_back(name);
+			}
+		}
+
+		if (!names.empty()) {
+			_sound.name = names[g_nancy->_randomSource->getRandomNumber(names.size() - 1)];
+		}
+
+		// Event flags fired on expiry (count at 0x73)
+		uint16 numFlags = stream.readUint16LE();
+		_flags.resize(numFlags);
+		for (uint i = 0; i < numFlags; ++i) {
+			_flags[i].label = stream.readSint16LE();
+			_flags[i].flag = (byte)stream.readSint16LE();
+		}
+		break;
+	}
+	default:
+		// kStart, kClear and kPause carry no further data
+		break;
+	}
 }
 
 void ResetAndStartTimer::execute() {
-	NancySceneState.resetAndStartTimer();
+	if (g_nancy->getGameType() < kGameTypeNancy12) {
+		NancySceneState.resetAndStartTimer();
+
+		// Nancy 11 also resets and starts one of the software-timer slots
+		if (g_nancy->getGameType() >= kGameTypeNancy11) {
+			TimerData::Timer *timer = getSoftwareTimer(_timerIndex);
+			if (timer) {
+				timer->reset();
+				timer->state = TimerData::Timer::kRunning;
+			}
+		}
 
-	// Nancy 11 also resets and starts one of the software-timer slots
-	if (g_nancy->getGameType() >= kGameTypeNancy11) {
-		TimerData::Timer *timer = getSoftwareTimer(_timerIndex);
-		if (timer) {
-			timer->reset();
+		_isDone = true;
+		return;
+	}
+
+	// Nancy 12+ issues a command to one of the software-timer slots. Every
+	// command other than kStart only takes effect while the slot is running.
+	TimerData::Timer *timer = getSoftwareTimer(_timerIndex);
+	if (timer) {
+		if (_command == kStart) {
 			timer->state = TimerData::Timer::kRunning;
+		} else if (timer->state == TimerData::Timer::kRunning) {
+			const uint32 durationMs = ((uint32)_hours * 3600 + (uint32)_minutes * 60 + (uint32)_seconds) * 1000;
+
+			switch (_command) {
+			case kClear:
+				timer->reset();
+				break;
+			case kPause:
+				timer->state = TimerData::Timer::kPaused;
+				break;
+			case kAddTime:
+				timer->currentTimeMs += durationMs;
+				break;
+			case kSubtractTime:
+				timer->currentTimeMs = timer->currentTimeMs > durationMs ? timer->currentTimeMs - durationMs : 0;
+				break;
+			case kSetTime:
+				timer->currentTimeMs = durationMs;
+				break;
+			case kConfigOneShot:
+			case kConfigRepeating:
+				addTimerTrigger(*timer,
+					(_command == kConfigRepeating) ? TimerData::Trigger::kRepeating : TimerData::Trigger::kOneShot,
+					durationMs, _sound, _flags);
+				break;
+			default:
+				break;
+			}
 		}
 	}
 
@@ -475,24 +632,6 @@ void StopTimer::execute() {
 	_isDone = true;
 }
 
-// Reads exactly size bytes from the stream, returning the text up to the first
-// null byte. Used for the fixed-size string fields inside a TimerControl chunk.
-static Common::String readFixedSizeString(Common::SeekableReadStream &stream, uint size) {
-	Common::String result;
-	bool ended = false;
-	for (uint i = 0; i < size; ++i) {
-		byte b = stream.readByte();
-		if (b == 0) {
-			ended = true;
-		}
-		if (!ended) {
-			result += (char)b;
-		}
-	}
-
-	return result;
-}
-
 void TimerControl::readData(Common::SeekableReadStream &stream) {
 	const int64 startPos = stream.pos();
 
diff --git a/engines/nancy/action/miscrecords.h b/engines/nancy/action/miscrecords.h
index 9dfeac38e59..e383914f898 100644
--- a/engines/nancy/action/miscrecords.h
+++ b/engines/nancy/action/miscrecords.h
@@ -285,14 +285,33 @@ protected:
 };
 
 // Starts the timer. Used in combination with Dependency types that check for
-// how much time has passed since the timer was started. From Nancy 11 onwards
-// the record also carries a software-timer slot index (see TimerControl).
+// how much time has passed since the timer was started. Nancy 11 also carries a
+// software-timer slot index (see TimerControl). From Nancy 12 the record became
+// a general "Control a Timer" command: a slot index plus a command whose value
+// selects a variable-size payload.
 class ResetAndStartTimer : public ActionRecord {
 public:
+	enum Command {
+		kStart           = 0, // Begin counting up from the current time
+		kClear           = 1, // Reset the slot back to idle
+		kConfigOneShot   = 2, // Set target/payload; fire once, then reset
+		kConfigRepeating = 3, // Set target/payload; fire once, then keep counting
+		kPause           = 4, // Suspend counting
+		kAddTime         = 5, // Add the duration to the elapsed time
+		kSubtractTime    = 6, // Subtract the duration from the elapsed time
+		kSetTime         = 7  // Set the elapsed time to the duration
+	};
+
 	void readData(Common::SeekableReadStream &stream) override;
 	void execute() override;
 
-	byte _timerIndex = 0; // Nancy 11+ software-timer slot
+	int16 _timerIndex = 0;   // Software-timer slot (Nancy 11+)
+	int16 _command = kStart; // Nancy 12+
+	int16 _hours = 0;
+	int16 _minutes = 0;
+	int16 _seconds = 0;
+	SoundDescription _sound;               // Played on expiry when configured
+	Common::Array<FlagDescription> _flags; // Fired on expiry when configured
 
 protected:
 	Common::String getRecordTypeName() const override { return "ResetAndStartTimer"; }
diff --git a/engines/nancy/nancy.h b/engines/nancy/nancy.h
index ad776b60a81..753c64f1572 100644
--- a/engines/nancy/nancy.h
+++ b/engines/nancy/nancy.h
@@ -54,7 +54,7 @@ class Serializer;
  */
 namespace Nancy {
 
-static const int kSavegameVersion = 5;
+static const int kSavegameVersion = 6;
 
 struct NancyGameDescription;
 
diff --git a/engines/nancy/puzzledata.cpp b/engines/nancy/puzzledata.cpp
index 797b2cea943..2d51a7d6679 100644
--- a/engines/nancy/puzzledata.cpp
+++ b/engines/nancy/puzzledata.cpp
@@ -396,6 +396,33 @@ void TimerData::synchronize(Common::Serializer &ser) {
 			ser.syncAsSint16LE(t.flags[j].label);
 			ser.syncAsByte(t.flags[j].flag);
 		}
+
+		// Nancy 12+ triggers, added in savegame version 6
+		if (ser.getVersion() >= 6) {
+			uint16 numTriggers = (uint16)t.triggers.size();
+			ser.syncAsUint16LE(numTriggers);
+			if (ser.isLoading()) {
+				t.triggers.resize(numTriggers);
+			}
+
+			for (uint j = 0; j < numTriggers; ++j) {
+				TimerData::Trigger &trig = t.triggers[j];
+				ser.syncAsSint32LE(trig.type);
+				ser.syncAsUint32LE(trig.durationMs);
+				ser.syncAsByte(trig.hasFired);
+
+				ser.syncString(trig.sound.name);
+				ser.syncAsUint16LE(trig.sound.channelID);
+				ser.syncAsUint16LE(trig.sound.playCommands);
+				ser.syncAsUint16LE(trig.sound.numLoops);
+				ser.syncAsUint16LE(trig.sound.volume);
+
+				for (uint k = 0; k < ARRAYSIZE(trig.flags); ++k) {
+					ser.syncAsSint16LE(trig.flags[k].label);
+					ser.syncAsByte(trig.flags[k].flag);
+				}
+			}
+		}
 	}
 }
 
diff --git a/engines/nancy/puzzledata.h b/engines/nancy/puzzledata.h
index bc4534db4c3..cbd13954866 100644
--- a/engines/nancy/puzzledata.h
+++ b/engines/nancy/puzzledata.h
@@ -267,11 +267,26 @@ struct CellPhonePictureData : public PuzzleData {
 };
 
 // Nancy 11+ AR 69 (TimerControl). 10 software timers, each counting up from
-// zero. A "configured" timer (state 5/6) fires a set of event flags, plays an
-// optional sound and shows an optional caption once its target duration
-// elapses. Started/stopped via ResetAndStartTimer (104) and StopTimer (105),
-// which in Nancy 11 carry a timer-slot index.
+// zero. In Nancy 11 a "configured" timer (state 5/6) fires a set of event flags,
+// plays an optional sound and shows an optional caption once its target duration
+// elapses. From Nancy 12 a running timer instead carries up to kNumTriggers
+// independent triggers (see ResetAndStartTimer), each firing its own flags and
+// sound. Started/stopped via ResetAndStartTimer (104) and StopTimer (105), which
+// in Nancy 11 carry a timer-slot index.
 struct TimerData : public PuzzleData {
+	// Nancy 12+ per-timer trigger: fires its flags and sound once its target
+	// duration is reached. A one-shot trigger clears the whole timer when it
+	// fires; a repeating one leaves the timer counting.
+	struct Trigger {
+		enum Type { kOneShot = 1, kRepeating = 2 };
+
+		int32 type = kOneShot;
+		uint32 durationMs = 0;
+		bool hasFired = false;
+		SoundDescription sound;
+		FlagDescription flags[10];
+	};
+
 	struct Timer {
 		enum State { kIdle = 0, kRunning = 1, kPaused = 2, kOneShot = 5, kRepeating = 6 };
 
@@ -283,11 +298,13 @@ struct TimerData : public PuzzleData {
 		Common::String autotextKey;
 		Common::String caption;
 		FlagDescription flags[10];
+		Common::Array<Trigger> triggers; // Nancy 12+
 
 		void reset() { *this = Timer(); }
 	};
 
 	static const uint kNumTimers = 10;
+	static const uint kNumTriggers = 20;
 
 	TimerData() {}
 	virtual ~TimerData() {}
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index f2bcc4ce91c..7e6bc40a142 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1391,6 +1391,7 @@ void Scene::tickSoftwareTimers(uint32 deltaMs) {
 
 		timer.currentTimeMs += deltaMs;
 
+		// Nancy 11 single-config timers fire directly from the timer state
 		if ((timer.state == TimerData::Timer::kOneShot || timer.state == TimerData::Timer::kRepeating) &&
 			timer.durationMs > 0 && !timer.hasFired && timer.currentTimeMs >= timer.durationMs) {
 			fireSoftwareTimer(timer);
@@ -1403,6 +1404,27 @@ void Scene::tickSoftwareTimers(uint32 deltaMs) {
 				timer.state = TimerData::Timer::kRunning;
 			}
 		}
+
+		// Nancy 12+ running timers fire from their triggers. A one-shot trigger
+		// clears the whole timer when it fires; a repeating one leaves it running.
+		if (timer.state == TimerData::Timer::kRunning) {
+			bool clearTimer = false;
+			for (uint j = 0; j < timer.triggers.size(); ++j) {
+				TimerData::Trigger &trigger = timer.triggers[j];
+				if (!trigger.hasFired && trigger.durationMs > 0 && timer.currentTimeMs >= trigger.durationMs) {
+					trigger.hasFired = true;
+					fireTimerTrigger(trigger);
+
+					if (trigger.type == TimerData::Trigger::kOneShot) {
+						clearTimer = true;
+					}
+				}
+			}
+
+			if (clearTimer) {
+				timer.reset();
+			}
+		}
 	}
 }
 
@@ -1454,6 +1476,30 @@ void Scene::fireSoftwareTimer(TimerData::Timer &timer) {
 	}
 }
 
+void Scene::fireTimerTrigger(TimerData::Trigger &trigger) {
+	// Set the trigger's event flags
+	for (uint i = 0; i < ARRAYSIZE(trigger.flags); ++i) {
+		if (trigger.flags[i].label != kFlagNoLabel) {
+			setEventFlag(trigger.flags[i]);
+		}
+	}
+
+	// Play the trigger's sound
+	if (trigger.sound.name != "NO SOUND") {
+		g_nancy->_sound->loadSound(trigger.sound);
+		g_nancy->_sound->playSound(trigger.sound);
+	}
+
+	// Nancy 12+ triggers carry no inline caption; the subtitle is looked up from
+	// the played sound's name
+	if (ConfMan.getBool("subtitles", ConfMan.getActiveDomainName()) && trigger.sound.name != "NO SOUND") {
+		const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
+		if (autotext && autotext->texts.contains(trigger.sound.name)) {
+			_textbox.addTextLine(autotext->texts[trigger.sound.name]);
+		}
+	}
+}
+
 Common::Rect Scene::activePopupConfinement() const {
 	// Pick the first visible Nancy 10+ popup; if more than one is open
 	// (shouldn't normally happen) the priority order matches the input
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index 9b82d09a945..1c9a7129cba 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -273,6 +273,7 @@ private:
 	// puzzle data) and fires any whose configured duration has just elapsed.
 	void tickSoftwareTimers(uint32 deltaMs);
 	void fireSoftwareTimer(TimerData::Timer &timer);
+	void fireTimerTrigger(TimerData::Trigger &trigger);
 
 	// Rect of the open Nancy 10+ taskbar popup, or empty if none.
 	Common::Rect activePopupConfinement() const;


Commit: 3aacdcadaa9ac33478f8344422589fdb090247f7
    https://github.com/scummvm/scummvm/commit/3aacdcadaa9ac33478f8344422589fdb090247f7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T19:29:25+03:00

Commit Message:
NANCY: NANCY12: Add new transparency mode

Transparency mode 3 has identical handling as mode 2. First observed
in Nancy12, SewingMachinePuzzle (the needle overlay)

Changed paths:
    engines/nancy/action/autotext.cpp
    engines/nancy/action/overlay.cpp
    engines/nancy/action/puzzle/peepholepuzzle.cpp
    engines/nancy/commontypes.h


diff --git a/engines/nancy/action/autotext.cpp b/engines/nancy/action/autotext.cpp
index e1761e4d404..9707156d702 100644
--- a/engines/nancy/action/autotext.cpp
+++ b/engines/nancy/action/autotext.cpp
@@ -187,7 +187,7 @@ void Autotext::execute() {
 		}
 
 		_fullSurface.create(surf, surf.getBounds());
-		if(_transparency == kPlayOverlayTransparent) {
+		if (_transparency >= kPlayOverlayTransparent) {
 			_fullSurface.setTransparentColor(g_nancy->_graphics->getTransColor());
 		}
 
@@ -224,7 +224,7 @@ void Autotext::execute() {
 			_viewportRender = new AutotextRender(_placementMode);
 			_viewportRender->_drawSurface.create(surf, src);
 			_viewportRender->moveTo(_viewportDest);
-			_viewportRender->setTransparent(_transparency == kPlayOverlayTransparent);
+			_viewportRender->setTransparent(_transparency >= kPlayOverlayTransparent);
 			_viewportRender->setVisible(true);
 			_viewportRender->init();
 			_viewportRender->registerGraphics();
diff --git a/engines/nancy/action/overlay.cpp b/engines/nancy/action/overlay.cpp
index 1b165274393..76787952e36 100644
--- a/engines/nancy/action/overlay.cpp
+++ b/engines/nancy/action/overlay.cpp
@@ -254,7 +254,7 @@ void Overlay::execute() {
 				}
 
 				_drawSurface.create(_fullSurface, srcRect);
-				setTransparent(_transparency == kPlayOverlayTransparent);
+				setTransparent(_transparency >= kPlayOverlayTransparent);
 
 				_currentFrame = nextFrame;
 				_needsRedraw = true;
@@ -330,7 +330,7 @@ void Overlay::execute() {
 
 						if (blitsForThisFrame.size() == 1) {
 							_drawSurface.create(_fullSurface, srcRect);
-							setTransparent(_transparency == kPlayOverlayTransparent);
+							setTransparent(_transparency >= kPlayOverlayTransparent);
 						} else {
 							Common::Rect d = _blitDescriptions[blitsForThisFrame[i]].dest;
 							d.translate(-destRect.left, -destRect.top);
diff --git a/engines/nancy/action/puzzle/peepholepuzzle.cpp b/engines/nancy/action/puzzle/peepholepuzzle.cpp
index 0d3460d2385..67273a35a35 100644
--- a/engines/nancy/action/puzzle/peepholepuzzle.cpp
+++ b/engines/nancy/action/puzzle/peepholepuzzle.cpp
@@ -55,7 +55,7 @@ void PeepholePuzzle::init() {
 
 	_currentSrc = _startSrc;
 
-	setTransparent(_transparency == kPlayOverlayTransparent);
+	setTransparent(_transparency >= kPlayOverlayTransparent);
 	_drawSurface.clear(_drawSurface.getTransparentColor());
 	setVisible(true);
 
diff --git a/engines/nancy/commontypes.h b/engines/nancy/commontypes.h
index 83cfe74af9c..c409cfd203c 100644
--- a/engines/nancy/commontypes.h
+++ b/engines/nancy/commontypes.h
@@ -104,9 +104,12 @@ static const byte kLargeVideoFormat					= 2;
 static const byte kVideoPlaytypeAVF					= 0;
 static const byte kVideoPlaytypeBink				= 1;
 
-// Overlay
+// Overlay transparency mode. 1 = opaque, anything >= kPlayOverlayTransparent is
+// drawn transparent (the original engine is 16bpp color-key, with no alpha/blend
+// path, so every transparent mode - Nancy12 also uses 3 - is a plain color key).
 static const byte kPlayOverlayPlain					= 1;
 static const byte kPlayOverlayTransparent			= 2;
+static const byte kPlayOverlayTransparent2			= 3;
 
 static const byte kPlayOverlaySceneChange			= 1;
 static const byte kPlayOverlayNoSceneChange			= 2;


Commit: 9c143cbe8d45bf93c7e8d659925031d601dddbc3
    https://github.com/scummvm/scummvm/commit/9c143cbe8d45bf93c7e8d659925031d601dddbc3
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T19:29:26+03:00

Commit Message:
NANCY: NANCY10: Bottom-align checkboxes in the task list

Fix #16965

Changed paths:
    engines/nancy/misc/hypertext.cpp


diff --git a/engines/nancy/misc/hypertext.cpp b/engines/nancy/misc/hypertext.cpp
index f24d0d858ca..f3cd1c78488 100644
--- a/engines/nancy/misc/hypertext.cpp
+++ b/engines/nancy/misc/hypertext.cpp
@@ -360,10 +360,14 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint leftOffse
 						Common::Rect markSrc = mark->_markSrcs[change.index];
 						Common::Rect markDest = markSrc;
 						if (useLineAlignedMark) {
-							// Nancy 10+: align the mark with the current
-							// line top, matching the original engine.
+							// Nancy 10+: bottom-anchor the mark to the current line,
+							// matching the original engine. The mark's baseline stays
+							// fixed while sprites of different heights (e.g. an unchecked
+							// box vs. a checked one) grow upward, so toggling a checkbox
+							// does not shift it vertically.
+							const int lineTop = textBounds.top + _numDrawnLines * lineStep(font) + _imageVerticalOffset;
 							markDest.moveTo(textBounds.left + horizontalOffset + (newLineStart ? 0 : leftOffsetNonNewline) + 1,
-								textBounds.top + _numDrawnLines * lineStep(font) + _imageVerticalOffset);
+								lineTop + font->getFontHeight() - markSrc.height());
 						} else {
 							markDest.moveTo(textBounds.left + horizontalOffset + (newLineStart ? 0 : leftOffsetNonNewline) + 1,
 								lineNumber == 0 ?




More information about the Scummvm-git-logs mailing list