[Scummvm-git-logs] scummvm master -> 818940106ec8b391b8fa6e481e3294f71b74f787
bluegr
noreply at scummvm.org
Wed May 13 00:15:23 UTC 2026
This automated email contains information about 6 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
688ebb1ec4 DEVTOOLS: Update game data for Nancy10-15 and regen nancy.dat
ed1bf47940 NANCY: Add a TODO for AR opcode 81, used in Nancy11+
934e300ec6 NANCY: More work on cursor handling of Nancy10+ games
2adf61f95b NANCY: Properly play the completion sound in onebuildpuzzle
f501ffecb7 NANCY: Fix close button placement in the Nancy10+ inventory popup
818940106e NANCY: More work on the Nancy10+ notebook popup
Commit: 688ebb1ec4493353f3eb2b98cdcd2521d4f35694
https://github.com/scummvm/scummvm/commit/688ebb1ec4493353f3eb2b98cdcd2521d4f35694
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-13T03:14:38+03:00
Commit Message:
DEVTOOLS: Update game data for Nancy10-15 and regen nancy.dat
Changed paths:
A devtools/create_nancy/nancy12_data.h
A devtools/create_nancy/nancy13_data.h
A devtools/create_nancy/nancy14_data.h
A devtools/create_nancy/nancy15_data.h
devtools/create_nancy/create_nancy.cpp
devtools/create_nancy/nancy10_data.h
devtools/create_nancy/nancy11_data.h
dists/engine-data/nancy.dat
diff --git a/devtools/create_nancy/create_nancy.cpp b/devtools/create_nancy/create_nancy.cpp
index b6fad3e20b1..e6a2776bbb2 100644
--- a/devtools/create_nancy/create_nancy.cpp
+++ b/devtools/create_nancy/create_nancy.cpp
@@ -33,11 +33,15 @@
#include "nancy9_data.h"
#include "nancy10_data.h"
#include "nancy11_data.h"
+#include "nancy12_data.h"
+#include "nancy13_data.h"
+#include "nancy14_data.h"
+#include "nancy15_data.h"
#define NANCYDAT_MAJOR_VERSION 1
#define NANCYDAT_MINOR_VERSION 1
-#define NANCYDAT_NUM_GAMES 12
+#define NANCYDAT_NUM_GAMES 16
/**
* Format specifications for nancy.dat:
@@ -77,6 +81,10 @@
* Nancy Drew: Danger on Deception Island
* Nancy Drew: The Secret of Shadow Ranch
* Nancy Drew: Curse of Blackmoor Manor
+ * Nancy Drew: Secret of the Old Clock
+ * Nancy Drew: Last Train to Blue Moon Canyon
+ * Nancy Drew: Danger by Design
+ * Nancy Drew: The Creature of Kapu Cave
*/
// Add the offset to the next tagged section before the section itself for easier navigation
@@ -346,6 +354,46 @@ int main(int argc, char *argv[]) {
WRAPWITHOFFSET(writeRingingTexts(output, _nancy8TelephoneRinging)) // same as 8
WRAPWITHOFFSET(writeEventFlagNames(output, _nancy11EventFlagNames))
+ // Nancy Drew: Secret of the Old Clock
+ gameOffsets.push_back(output.pos());
+ WRAPWITHOFFSET(writeConstants(output, _nancy12Constants))
+ WRAPWITHOFFSET(writeSoundChannels(output, _nancy3andUpSoundChannelInfo)) // same as 3
+ WRAPWITHOFFSET(writeLanguages(output, _nancy8LanguagesOrder)) // same as 8
+ WRAPWITHOFFSET(writeConditionalDialogue(output, _nancy12ConditionalDialogue))
+ WRAPWITHOFFSET(writeGoodbyes(output, _nancy12Goodbyes))
+ WRAPWITHOFFSET(writeRingingTexts(output, _nancy8TelephoneRinging)) // same as 8
+ WRAPWITHOFFSET(writeEventFlagNames(output, _nancy12EventFlagNames))
+
+ // Nancy Drew: Last Train to Blue Moon Canyon
+ gameOffsets.push_back(output.pos());
+ WRAPWITHOFFSET(writeConstants(output, _nancy13Constants))
+ WRAPWITHOFFSET(writeSoundChannels(output, _nancy3andUpSoundChannelInfo)) // same as 3
+ WRAPWITHOFFSET(writeLanguages(output, _nancy8LanguagesOrder)) // same as 8
+ WRAPWITHOFFSET(writeConditionalDialogue(output, _nancy12ConditionalDialogue)) // same as 12
+ WRAPWITHOFFSET(writeGoodbyes(output, _nancy12Goodbyes)) // same as 12
+ WRAPWITHOFFSET(writeRingingTexts(output, _nancy8TelephoneRinging)) // same as 8
+ WRAPWITHOFFSET(writeEventFlagNames(output, _nancy12EventFlagNames)) // same as 12
+
+ // Nancy Drew: Danger by Design
+ gameOffsets.push_back(output.pos());
+ WRAPWITHOFFSET(writeConstants(output, _nancy14Constants))
+ WRAPWITHOFFSET(writeSoundChannels(output, _nancy3andUpSoundChannelInfo)) // same as 3
+ WRAPWITHOFFSET(writeLanguages(output, _nancy8LanguagesOrder)) // same as 8
+ WRAPWITHOFFSET(writeConditionalDialogue(output, _nancy12ConditionalDialogue)) // same as 12
+ WRAPWITHOFFSET(writeGoodbyes(output, _nancy12Goodbyes)) // same as 12
+ WRAPWITHOFFSET(writeRingingTexts(output, _nancy8TelephoneRinging)) // same as 8
+ WRAPWITHOFFSET(writeEventFlagNames(output, _nancy12EventFlagNames)) // same as 12
+
+ // Nancy Drew: The Creature of Kapu Cave
+ gameOffsets.push_back(output.pos());
+ WRAPWITHOFFSET(writeConstants(output, _nancy15Constants))
+ WRAPWITHOFFSET(writeSoundChannels(output, _nancy3andUpSoundChannelInfo)) // same as 3
+ WRAPWITHOFFSET(writeLanguages(output, _nancy8LanguagesOrder)) // same as 8
+ WRAPWITHOFFSET(writeConditionalDialogue(output, _nancy12ConditionalDialogue)) // same as 12
+ WRAPWITHOFFSET(writeGoodbyes(output, _nancy12Goodbyes)) // same as 12
+ WRAPWITHOFFSET(writeRingingTexts(output, _nancy8TelephoneRinging)) // same as 8
+ WRAPWITHOFFSET(writeEventFlagNames(output, _nancy12EventFlagNames)) // same as 12
+
// Write the offsets for each game in the header
output.seek(offsetsOffset);
for (uint i = 0; i < gameOffsets.size(); ++i) {
diff --git a/devtools/create_nancy/nancy10_data.h b/devtools/create_nancy/nancy10_data.h
index 2b5148302a5..c5bc8d91995 100644
--- a/devtools/create_nancy/nancy10_data.h
+++ b/devtools/create_nancy/nancy10_data.h
@@ -30,7 +30,7 @@ const GameConstants _nancy10Constants ={
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // genericEventFlags
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30 },
- 20, // numCursorTypes
+ 37, // numCursorTypes
4000, // logoEndAfter
32 // wonGameFlagID
};
diff --git a/devtools/create_nancy/nancy11_data.h b/devtools/create_nancy/nancy11_data.h
index 8a9c416ffeb..a656609c0cf 100644
--- a/devtools/create_nancy/nancy11_data.h
+++ b/devtools/create_nancy/nancy11_data.h
@@ -30,7 +30,7 @@ const GameConstants _nancy11Constants ={
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // genericEventFlags
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30 },
- 20, // numCursorTypes
+ 37, // numCursorTypes
4000, // logoEndAfter
32 // wonGameFlagID
};
diff --git a/devtools/create_nancy/nancy12_data.h b/devtools/create_nancy/nancy12_data.h
new file mode 100644
index 00000000000..efab0a8ad95
--- /dev/null
+++ b/devtools/create_nancy/nancy12_data.h
@@ -0,0 +1,45 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY12DATA_H
+#define NANCY12DATA_H
+
+#include "types.h"
+
+const GameConstants _nancy12Constants ={
+ 70, // numItems
+ 1251, // numEventFlags - TODO: verify this
+ { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // genericEventFlags
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 },
+ 37, // numCursorTypes
+ 4000, // logoEndAfter
+ 32 // wonGameFlagID
+};
+
+// Conditional dialog checks and goodbyes have been moved to game data files
+const Common::Array<Common::Array<ConditionalDialogue>> _nancy12ConditionalDialogue = {};
+const Common::Array<Goodbye> _nancy12Goodbyes = {};
+
+// Event flag names are no longer stored in the executable
+const Common::Array<const char *> _nancy12EventFlagNames = {};
+
+#endif // NANCY12DATA_H
diff --git a/devtools/create_nancy/nancy13_data.h b/devtools/create_nancy/nancy13_data.h
new file mode 100644
index 00000000000..aa7077ec490
--- /dev/null
+++ b/devtools/create_nancy/nancy13_data.h
@@ -0,0 +1,38 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY13DATA_H
+#define NANCY13DATA_H
+
+#include "types.h"
+
+const GameConstants _nancy13Constants ={
+ 50, // numItems
+ 1251, // numEventFlags - TODO: verify this
+ { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // genericEventFlags
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 },
+ 37, // numCursorTypes
+ 4000, // logoEndAfter
+ 32 // wonGameFlagID
+};
+
+#endif // NANCY13DATA_H
diff --git a/devtools/create_nancy/nancy14_data.h b/devtools/create_nancy/nancy14_data.h
new file mode 100644
index 00000000000..ae7a1301cc3
--- /dev/null
+++ b/devtools/create_nancy/nancy14_data.h
@@ -0,0 +1,38 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY14DATA_H
+#define NANCY14DATA_H
+
+#include "types.h"
+
+const GameConstants _nancy14Constants ={
+ 50, // numItems
+ 1251, // numEventFlags - TODO: verify this
+ { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // genericEventFlags
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 },
+ 44, // numCursorTypes
+ 4000, // logoEndAfter
+ 32 // wonGameFlagID
+};
+
+#endif // NANCY14DATA_H
diff --git a/devtools/create_nancy/nancy15_data.h b/devtools/create_nancy/nancy15_data.h
new file mode 100644
index 00000000000..3280ed52a60
--- /dev/null
+++ b/devtools/create_nancy/nancy15_data.h
@@ -0,0 +1,38 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY15DATA_H
+#define NANCY15DATA_H
+
+#include "types.h"
+
+const GameConstants _nancy15Constants ={
+ 50, // numItems
+ 1251, // numEventFlags - TODO: verify this
+ { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // genericEventFlags
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 },
+ 44, // numCursorTypes
+ 4000, // logoEndAfter
+ 32 // wonGameFlagID
+};
+
+#endif // NANCY15DATA_H
diff --git a/dists/engine-data/nancy.dat b/dists/engine-data/nancy.dat
index c7a0d53098e..aad04f0d99b 100644
Binary files a/dists/engine-data/nancy.dat and b/dists/engine-data/nancy.dat differ
Commit: ed1bf47940ae63b4a0d2196ee9b6feee8716c38f
https://github.com/scummvm/scummvm/commit/ed1bf47940ae63b4a0d2196ee9b6feee8716c38f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-13T03:14:41+03:00
Commit Message:
NANCY: Add a TODO for AR opcode 81, used in Nancy11+
This prevents crashing when starting Nancy11
Changed paths:
engines/nancy/action/arfactory.cpp
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index eb51a53fa2e..0c741ba7905 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -250,7 +250,6 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new ModifyListEntry(ModifyListEntry::kMark);
case 74: // Added in Nancy 10
case 75: // Changed in Nancy 10
- case 81: // Nancy 11+
if (g_nancy->getGameType() <= kGameTypeNancy9 && type == 75) {
return new TextBoxWrite();
} else {
@@ -264,6 +263,8 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new SetValueCombo();
case 79:
return new ValueTest();
+ //case 81: // Nancy 11+
+ // return nullptr; // TODO
case 97:
return new EventFlags(true);
case 98:
Commit: 934e300ec6b255b07f4e1f2469471683dff2411a
https://github.com/scummvm/scummvm/commit/934e300ec6b255b07f4e1f2469471683dff2411a
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-13T03:14:43+03:00
Commit Message:
NANCY: More work on cursor handling of Nancy10+ games
This fixes cursors for Nancy10-12. Still need to add handling for split
UI + inventory cursors in Nancy13+
Changed paths:
engines/nancy/cursor.cpp
engines/nancy/cursor.h
diff --git a/engines/nancy/cursor.cpp b/engines/nancy/cursor.cpp
index 7ca246e4a32..4e81f2d600a 100644
--- a/engines/nancy/cursor.cpp
+++ b/engines/nancy/cursor.cpp
@@ -45,7 +45,7 @@ void CursorManager::init(Common::SeekableReadStream *chunkStream) {
assert(chunkStream);
chunkStream->seek(0);
- // First, we need to figure out the number of possible CursorTypes in the current game
+ // First, we need to figure out the number of possible CursorTypes in the current game.
_numCursorTypes = g_nancy->getStaticData().numCursorTypes;
// The structure of CURS is weird:
@@ -70,16 +70,17 @@ void CursorManager::init(Common::SeekableReadStream *chunkStream) {
// the ones in the item arrays. As a result, most of the CURS data is effectively junk that never gets used.
// Perhaps in the future the class could be modified so we no longer have to store or care about all of the junk cursors;
- // however, this cannot happen until the engine is more mature and I'm more aware of what changes they made to the
+ // however, this cannot happen until the engine is more mature and we're more aware of what changes they made to the
// cursor code in later games.
- uint numCursors = _numCursorTypes * (g_nancy->getGameType() == kGameTypeVampire ? 2 : 3) + g_nancy->getStaticData().numItems * _numCursorTypes;
- if (g_nancy->getGameType() >= kGameTypeNancy10 && g_nancy->getGameType() <= kGameTypeNancy12) {
- _numCursorTypes = 37;
- numCursors = _numCursorTypes * 2 + g_nancy->getStaticData().numItems * 2;
- } else if (g_nancy->getGameType() >= kGameTypeNancy13) {
- _numCursorTypes = 37;
- numCursors = 174;
+ uint numCursors;
+ if (g_nancy->getGameType() >= kGameTypeNancy10) {
+ // Normal + item cursors. Each cursor has a normal and a highlighted variant
+ numCursors = (_numCursorTypes + g_nancy->getStaticData().numItems) * 2;
+ } else {
+ const uint sysSections = (g_nancy->getGameType() == kGameTypeVampire) ? 2 : 3;
+ numCursors = _numCursorTypes * sysSections
+ + g_nancy->getStaticData().numItems * _numCursorTypes;
}
_cursors.resize(numCursors);
@@ -88,7 +89,7 @@ void CursorManager::init(Common::SeekableReadStream *chunkStream) {
Common::Path inventoryCursorsImageName;
if (g_nancy->getGameType() <= kGameTypeNancy12) {
- auto *inventoryData = GetEngineData(INV)
+ auto *inventoryData = GetEngineData(INV);
assert(inventoryData);
inventoryCursorsImageName = inventoryData->inventoryCursorsImageName;
} else {
@@ -100,6 +101,9 @@ void CursorManager::init(Common::SeekableReadStream *chunkStream) {
readRect(*chunkStream, _cursors[i].bounds);
}
+ // Nancy 10-12 store a parallel rect array (likely highlighted-state
+ // variants of the same cursors) between the source rects and the
+ // hotspot block.
if (g_nancy->getGameType() >= kGameTypeNancy10 && g_nancy->getGameType() <= kGameTypeNancy12)
chunkStream->skip(numCursors * 4 * 4); // TODO
@@ -112,8 +116,10 @@ void CursorManager::init(Common::SeekableReadStream *chunkStream) {
_primaryVideoInitialPos.x = chunkStream->readUint16LE();
_primaryVideoInitialPos.y = chunkStream->readUint16LE();
- if (g_nancy->getGameType() >= kGameTypeNancy13)
+ if (g_nancy->getGameType() >= kGameTypeNancy13) {
g_nancy->_resource->loadImage(uiCursorsImageName, _uiCursorsSurface);
+ // TODO: Add handling for split UI + inventory cursors in Nancy13+
+ }
g_nancy->_resource->loadImage(inventoryCursorsImageName, _invCursorsSurface);
@@ -127,139 +133,156 @@ void CursorManager::init(Common::SeekableReadStream *chunkStream) {
delete chunkStream;
}
+uint CursorManager::resolveNancy10CursorID(CursorType type, int16 itemID) {
+ // Item-held variants. The Nancy 10+ chunk reserves `numItems à 2`
+ // slots after the two 37-entry system arrays (= _numCursorTypes * 2),
+ // each item getting one [idle, hotspot] pair. Held items only
+ // override the cursor for kNormal / kHotspot; directional /
+ // rotate / arrow types render their system sprite directly even
+ // while the player is carrying something.
+ if (itemID != -1 && (type == kNormal || type == kHotspot)) {
+ _hasItem = true;
+ const uint itemsOffset = (uint)_numCursorTypes * 2;
+ const uint variant = (type == kHotspot) ? 1 : 0;
+ return itemsOffset + (uint)itemID * 2 + variant;
+ }
+
+ // System cursors: translate the legacy CursorType to the matching
+ // kNew* idle slot. Each Nancy 10+ cursor type T occupies a pair
+ // (T*2, T*2+1) in the chunk â idle followed by hotspot. We always
+ // return the idle slot here
+ switch (type) {
+ case kNormal: return kNewNormal;
+ case kHotspot: return kNewHotspot;
+ case kNormalArrow: return kNewNormalArrow;
+ case kHotspotArrow: return kNewHotspotArrow;
+ case kExit: return kNewExit;
+ case kMove: return kNewExit;
+ case kRotateCW: return kNewRotateCW;
+ case kRotateCCW: return kNewRotateCCW;
+ case kMoveLeft: return kNewMoveLeft;
+ case kMoveRight: return kNewMoveRight;
+ case kMoveForward: return kNewMoveForward;
+ case kMoveBackward: return kNewMoveBackward;
+ case kMoveUp: return kNewMoveUp;
+ case kMoveDown: return kNewMoveDown;
+ case kRotateLeft: return kNewRotateLeft;
+ case kRotateRight: return kNewRotateRight;
+ case kInvertedRotateLeft: return kNewInvertedRotateLeft;
+ case kInvertedRotateRight: return kNewInvertedRotateRight;
+ default:
+ return kNewNormal;
+ }
+}
+
void CursorManager::setCursor(CursorType type, int16 itemID) {
- if (!_isInitialized) {
+ if (!_isInitialized)
return;
- }
- GameType gameType = g_nancy->getGameType();
+ const GameType gameType = g_nancy->getGameType();
- if (type == _curCursorType && itemID == _curItemID) {
+ if (type == _curCursorType && itemID == _curItemID)
return;
- } else {
- _curCursorType = type;
- _curItemID = itemID;
- }
+ _curCursorType = type;
+ _curItemID = itemID;
_hasItem = false;
- // For all cases below, the selected cursor is _always_ shown, regardless
- // of whether or not an item is held. All other types of cursor
- // are overridable when holding an item. Every item cursor has
- // _numItemCursor variants, one corresponding to every numbered
+ if (gameType >= kGameTypeNancy10) {
+ _curCursorID = resolveNancy10CursorID(type, itemID);
+ return;
+ }
+
+ // For all cases below, the selected cursor is _always_ shown,
+ // regardless of whether or not an item is held. All other types of
+ // cursor are overridable when holding an item. Every item cursor
+ // has _numCursorTypes variants, one corresponding to every numbered
// value of the CursorType enum.
+
switch (type) {
case kNormalArrow:
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewNormalArrow: _numCursorTypes;
+ _curCursorID = _numCursorTypes;
return;
case kHotspotArrow:
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewHotspotArrow : _numCursorTypes + 1;
+ _curCursorID = _numCursorTypes + 1;
return;
case kInvertedRotateLeft:
- // Only valid for nancy6 and up
if (gameType >= kGameTypeNancy6) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewInvertedRotateLeft : kInvertedRotateLeft;
+ _curCursorID = kInvertedRotateLeft;
return;
}
-
// fall through
case kRotateLeft:
- // Only valid for nancy6 and up
if (gameType >= kGameTypeNancy6) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewRotateLeft : kRotateLeft;
+ _curCursorID = kRotateLeft;
return;
}
-
// fall through
case kMoveLeft:
- // Only valid for nancy3 and up
if (gameType >= kGameTypeNancy3) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewMoveLeft : kMoveLeft;
+ _curCursorID = kMoveLeft;
return;
- } else {
- type = kMove;
}
-
+ type = kMove;
break;
case kInvertedRotateRight:
- // Only valid for nancy6 and up
if (gameType >= kGameTypeNancy6) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewInvertedRotateRight : kInvertedRotateRight;
+ _curCursorID = kInvertedRotateRight;
return;
}
-
// fall through
case kRotateRight:
- // Only valid for nancy6 and up
if (gameType >= kGameTypeNancy6) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewRotateRight : kRotateRight;
+ _curCursorID = kRotateRight;
return;
}
-
// fall through
case kMoveRight:
- // Only valid for nancy3 and up
if (gameType >= kGameTypeNancy3) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewMoveRight : kMoveRight;
+ _curCursorID = kMoveRight;
return;
- } else {
- type = kMove;
}
-
+ type = kMove;
break;
case kMoveUp:
- // Only valid for nancy4 and up
if (gameType >= kGameTypeNancy4) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewMoveUp : kMoveUp;
+ _curCursorID = kMoveUp;
return;
- } else {
- type = kMove;
}
-
+ type = kMove;
break;
case kMoveDown:
- // Only valid for nancy4 and up
if (gameType >= kGameTypeNancy4) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewMoveDown : kMoveDown;
+ _curCursorID = kMoveDown;
return;
- } else {
- type = kMove;
}
-
+ type = kMove;
break;
case kMoveForward:
- // Only valid for nancy4 and up
if (gameType >= kGameTypeNancy4) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewMoveForward : kMoveForward;
+ _curCursorID = kMoveForward;
return;
- } else {
- type = kHotspot;
}
-
+ type = kHotspot;
break;
case kMoveBackward:
- // Only valid for nancy4 and up
if (gameType >= kGameTypeNancy4) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewMoveBackward : kMoveBackward;
+ _curCursorID = kMoveBackward;
return;
- } else {
- type = kHotspot;
}
-
+ type = kHotspot;
break;
case kExit:
- // Not valid in TVD
if (gameType != kGameTypeVampire) {
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewExit : kExit;
+ _curCursorID = kExit;
return;
}
-
break;
case kRotateCW:
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewRotateCW : kRotateCW;
+ _curCursorID = kRotateCW;
return;
case kRotateCCW:
- _curCursorID = (gameType >= kGameTypeNancy10) ? kNewRotateCCW : kRotateCCW;
+ _curCursorID = kRotateCCW;
return;
default:
break;
@@ -272,12 +295,11 @@ void CursorManager::setCursor(CursorType type, int16 itemID) {
// No item held, set to eyeglass
itemID = 0;
} else {
- // Item held
- itemsOffset = _numCursorTypes * (g_nancy->getGameType() == kGameTypeVampire ? 2 : 3);
+ itemsOffset = _numCursorTypes * (gameType == kGameTypeVampire ? 2 : 3);
_hasItem = true;
}
- _curCursorID = (itemID * _numCursorTypes) + itemsOffset + type;
+ _curCursorID = (uint)(itemID * _numCursorTypes) + itemsOffset + (uint)type;
}
void CursorManager::setCursorType(CursorType type) {
@@ -327,9 +349,8 @@ void CursorManager::showCursor(bool shouldShow) {
}
void CursorManager::adjustCursorHotspot() {
- if (g_nancy->getGameType() == kGameTypeVampire) {
+ if (g_nancy->getGameType() == kGameTypeVampire)
return;
- }
// Improvement: the arrow cursor in the Nancy games has an atrocious hotspot that's
// right in the middle of the graphic, instead of in the top left where
@@ -339,13 +360,15 @@ void CursorManager::adjustCursorHotspot() {
// TODO: Make this optional?
- uint startID = _curCursorID;
+ const CursorType startType = _curCursorType;
+ const uint startID = _curCursorID;
setCursorType(kNormalArrow);
_cursors[_curCursorID].hotspot = {3, 4};
setCursorType(kHotspotArrow);
_cursors[_curCursorID].hotspot = {3, 4};
+ _curCursorType = startType;
_curCursorID = startID;
}
diff --git a/engines/nancy/cursor.h b/engines/nancy/cursor.h
index c4b3834f9ac..690d362aa3f 100644
--- a/engines/nancy/cursor.h
+++ b/engines/nancy/cursor.h
@@ -56,24 +56,26 @@ public:
kNormalArrow = 20,
kHotspotArrow = 21,
- // Cursors in Nancy10 and newer games
- kNewNormal = 0, // Eyeglass, non-highlighted
- kNewHotspot = 1, // Eyeglass, highlighted
- kNewNormalArrow = 8,
- kNewHotspotArrow = 9,
- kNewExit = 10, // Used for movement and exiting puzzles
- kNewRotateCW = 13, // Used in puzzles only
- kNewRotateCCW = 14, // Used in puzzles only
- kNewMoveLeft = 15, // Used for movement and turning in 360 scenes
- kNewMoveRight = 16, // Used for movement and turning in 360 scenes
- kNewMoveForward = 17, // Used for movement
- kNewMoveBackward = 18, // Used for movement and exiting puzzles
- kNewMoveUp = 19, // Used for movement
- kNewMoveDown = 20, // Used for movement
- kNewRotateRight = 21, // Used in 360 scenes
- kNewRotateLeft = 22, // Used in 360 scenes
- kNewInvertedRotateRight = 23, // Used in 360 scenes
- kNewInvertedRotateLeft = 24, // Used in 360 scenes
+ // Cursors in Nancy10 and newer games. Each cursor type stores
+ // two consecutive entries in the chunk: an idle slot at
+ // (type * 2) and a hotspot/highlighted slot at (type * 2 + 1).
+ kNewNormal = 0, // Type 0 idle â Eyeglass
+ kNewHotspot = 1, // Type 0 hotspot â Eyeglass highlighted (only "hotspot" variant we expose)
+ kNewNormalArrow = 8, // Type 4 idle â when the cursor is over the taskbar
+ kNewHotspotArrow = 9, // Type 4 hotspot
+ kNewExit = 10, // Type 5 idle â Used for movement and exiting puzzles
+ kNewRotateCW = 12, // Type 6 idle â Used in puzzles only
+ kNewRotateCCW = 14, // Type 7 idle â Used in puzzles only
+ kNewMoveLeft = 16, // Type 8 idle â Used for movement and turning in 360 scenes
+ kNewMoveRight = 18, // Type 9 idle â Used for movement and turning in 360 scenes
+ kNewMoveForward = 20, // Type 10 idle â Used for movement
+ kNewMoveBackward = 22, // Type 11 idle â Used for movement and exiting puzzles
+ kNewMoveUp = 24, // Type 12 idle â Used for movement
+ kNewMoveDown = 26, // Type 13 idle â Used for movement
+ kNewRotateRight = 28, // Type 14 idle â Used in 360 scenes
+ kNewRotateLeft = 30, // Type 15 idle â Used in 360 scenes
+ kNewInvertedRotateRight = 32, // Type 16 idle â Used in 360 scenes
+ kNewInvertedRotateLeft = 34, // Type 17 idle â Used in 360 scenes
};
CursorManager();
@@ -100,6 +102,9 @@ public:
private:
void adjustCursorHotspot();
+ // Resolve a CursorType + held-item pair to a Nancy 10+ cursor ID.
+ uint resolveNancy10CursorID(CursorType type, int16 itemID);
+
struct Cursor {
Common::Rect bounds;
Common::Point hotspot;
Commit: 2adf61f95b78e8145fdab1d8572f0b4a1f7241a7
https://github.com/scummvm/scummvm/commit/2adf61f95b78e8145fdab1d8572f0b4a1f7241a7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-13T03:14:46+03:00
Commit Message:
NANCY: Properly play the completion sound in onebuildpuzzle
The completion sound is played on the same channel as the piece
placement sound, so it needs to be loaded before being played
Fix #16789
Changed paths:
engines/nancy/action/puzzle/onebuildpuzzle.cpp
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index 161f4109a8b..a49b1adfb4d 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -198,8 +198,9 @@ void OneBuildPuzzle::execute() {
// Pickup/rotate sound finished; return to idle (piece still dragging)
_solveState = kIdle;
} else if (_correctlyPlaced) {
- playGoodPlacementSound();
checkAllPlaced();
+ if (!_isSolved)
+ playGoodPlacementSound();
} else {
// Wrong drop: play bad placement feedback
playBadPlacementSound();
@@ -222,6 +223,7 @@ void OneBuildPuzzle::execute() {
break;
case kTriggerCompletion:
// Play completion sound/text, then wait for it to finish
+ g_nancy->_sound->loadSound(_completionSound);
g_nancy->_sound->playSound(_completionSound);
if (!_completionText.empty()) {
NancySceneState.getTextbox().clear();
Commit: f501ffecb76b6e7fb34be01d773d04519561023a
https://github.com/scummvm/scummvm/commit/f501ffecb76b6e7fb34be01d773d04519561023a
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-13T03:14:48+03:00
Commit Message:
NANCY: Fix close button placement in the Nancy10+ inventory popup
Changed paths:
engines/nancy/ui/inventorypopup.cpp
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index a545ad1d5ab..a352af534b0 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -219,10 +219,15 @@ void InventoryPopup::updatePageFromScroll() {
void InventoryPopup::drawCloseButton(WidgetState state) {
const UIButtonRecord &btn = _uiivData->header.secondaryButton;
Common::Rect spr = btn.sourceRects[state];
- const Common::Point chunkOrigin(_uiivData->header.normalDestRect.left,
- _uiivData->header.normalDestRect.top);
- const Common::Point dst(btn.destRect.left - chunkOrigin.x,
- btn.destRect.top - chunkOrigin.y);
+ Common::Rect dstRect = btn.destRect;
+ if (btn.destUsesGameFrameOffset) {
+ const VIEW *view = GetEngineData(VIEW);
+ if (view) {
+ dstRect.translate(view->screenPosition.left, view->screenPosition.top);
+ }
+ }
+ const Common::Point dst(dstRect.left - _screenPosition.left,
+ dstRect.top - _screenPosition.top);
_drawSurface.blitFrom(_overlayImage, spr, dst);
}
@@ -353,8 +358,15 @@ void InventoryPopup::handleInput(NancyInput &input) {
}
if (_uiivData->header.secondaryButtonEnabled) {
- const Common::Rect &closeRect = _uiivData->header.secondaryButton.destRect;
- const bool overClose = closeRect.contains(chunkMouse);
+ const UIButtonRecord &closeBtn = _uiivData->header.secondaryButton;
+ Common::Rect closeScreen = closeBtn.destRect;
+ if (closeBtn.destUsesGameFrameOffset) {
+ const VIEW *view = GetEngineData(VIEW);
+ if (view) {
+ closeScreen.translate(view->screenPosition.left, view->screenPosition.top);
+ }
+ }
+ const bool overClose = closeScreen.contains(input.mousePos);
if (overClose != _closeButtonHovered) {
_closeButtonHovered = overClose;
drawCloseButton(overClose ? kStateHover : kStateIdle);
Commit: 818940106ec8b391b8fa6e481e3294f71b74f787
https://github.com/scummvm/scummvm/commit/818940106ec8b391b8fa6e481e3294f71b74f787
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-13T03:14:49+03:00
Commit Message:
NANCY: More work on the Nancy10+ notebook popup
- Implement text rendering
- Implement text scrolling
- Implement tab switching between journal and tasks
Changed paths:
engines/nancy/action/datarecords.cpp
engines/nancy/state/scene.h
engines/nancy/ui/notebookpopup.cpp
engines/nancy/ui/notebookpopup.h
diff --git a/engines/nancy/action/datarecords.cpp b/engines/nancy/action/datarecords.cpp
index 6c4e26cd288..e001ee07e2f 100644
--- a/engines/nancy/action/datarecords.cpp
+++ b/engines/nancy/action/datarecords.cpp
@@ -468,6 +468,12 @@ void ModifyListEntry::execute() {
break;
}
+ // Nancy 10+: if the notebook popup is currently visible, refresh the
+ // rendered list, so the new/changed entry shows up.
+ if (g_nancy->getGameType() >= kGameTypeNancy10 && NancySceneState.getNotebookPopup().isOpen()) {
+ NancySceneState.getNotebookPopup().refreshContent();
+ }
+
finishExecution();
}
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index 6cbaedab79b..d7095756b93 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -173,6 +173,8 @@ public:
UI::Viewport &getViewport() { return _viewport; }
UI::Textbox &getTextbox() { return _textbox; }
UI::InventoryBox &getInventoryBox() { return _inventoryBox; }
+ UI::InventoryPopup &getInventoryPopup() { return _inventoryPopup; }
+ UI::NotebookPopup &getNotebookPopup() { return _notebookPopup; }
UI::Clock *getClock();
UI::Taskbar *getTaskbar() { return _taskbar; }
diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
index 5f23126affe..e0d9f3de114 100644
--- a/engines/nancy/ui/notebookpopup.cpp
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -20,12 +20,16 @@
*/
#include "engines/nancy/cursor.h"
+#include "engines/nancy/font.h"
#include "engines/nancy/graphics.h"
#include "engines/nancy/input.h"
#include "engines/nancy/nancy.h"
+#include "engines/nancy/puzzledata.h"
#include "engines/nancy/resource.h"
#include "engines/nancy/sound.h"
+#include "engines/nancy/state/scene.h"
+
#include "engines/nancy/ui/notebookpopup.h"
namespace Nancy {
@@ -39,12 +43,24 @@ NotebookPopup::NotebookPopup() :
_isOpen(false),
_activeTab(0) {}
+// Cap on how tall HypertextParser's working surface can grow. Notebook
+// journals on Nancy 10+ rarely exceed a few hundred wrapped lines; this
+// gives plenty of headroom while keeping the allocation bounded.
+static const uint16 kHypertextSurfaceHeight = 4096;
+
void NotebookPopup::init() {
_uinbData = GetEngineData(UINB);
assert(_uinbData);
g_nancy->_resource->loadImage(_uinbData->header.imageName, _overlayImage);
+ // Close (X) button image.
+ if (_uinbData->header.secondaryButtonEnabled &&
+ !_uinbData->header.secondaryButton.primaryImageName.empty()) {
+ g_nancy->_resource->loadImage(_uinbData->header.secondaryButton.primaryImageName,
+ _closeButtonImage);
+ }
+
Common::Rect popupRect = _uinbData->header.normalDestRect;
if (_uinbData->header.overlayInGameFrame) {
const VIEW *view = GetEngineData(VIEW);
@@ -57,6 +73,16 @@ void NotebookPopup::init() {
bounds.moveTo(0, 0);
_drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getInputPixelFormat());
+ // Set up HypertextParser's scratch surfaces. Width matches the
+ // chunk's text rect; height is generously oversized so journal
+ // content for any plausible save state fits without truncation
+ // (overflow is handled by scrolling, not by growing the surface).
+ // Background color is irrelevant â paintPaperIntoFullSurface()
+ // re-tiles the popup's paper texture after every clear() so the
+ // text always sits on real notebook paper.
+ initSurfaces(_uinbData->textRect.width(), kHypertextSurfaceHeight,
+ g_nancy->_graphics->getInputPixelFormat(), 0, 0);
+
// Pick the first enabled tab as the initially active one
_activeTab = 0;
for (uint i = 0; i < kNumTabs; ++i) {
@@ -68,8 +94,8 @@ void NotebookPopup::init() {
drawBackground();
drawTabs();
-
- // TODO: Draw the actual notebook page contents
+ drawContent();
+ drawForeground();
setTransparent(false);
setVisible(false);
@@ -82,12 +108,16 @@ void NotebookPopup::registerGraphics() {
}
void NotebookPopup::open() {
- if (_isOpen) {
+ if (_isOpen)
return;
- }
+
_isOpen = true;
setVisible(true);
+ // JournalData entries may have changed since the last open (added by
+ // ModifyListEntry, marked complete, etc.) â re-render content.
+ refreshContent();
+
if (!_uinbData->header.sounds[0].name.empty()) {
g_nancy->_sound->loadSound(_uinbData->header.sounds[0]);
g_nancy->_sound->playSound(_uinbData->header.sounds[0]);
@@ -111,63 +141,232 @@ void NotebookPopup::drawBackground() {
_drawSurface.blitFrom(_overlayImage, _uinbData->header.normalSrcRect, Common::Point(0, 0));
}
-void NotebookPopup::drawTabs() {
- // Sub-rects in the chunk are stored relative to header.normalDestRect.
- // After we translate the popup by the viewport offset, _screenPosition
- // no longer matches that origin, so popup-local conversions must
- // subtract chunk.normalDestRect.topLeft, not _screenPosition.
- const Common::Point chunkOrigin(_uinbData->header.normalDestRect.left,
- _uinbData->header.normalDestRect.top);
+void NotebookPopup::drawForeground() {
+ drawCloseButton(_closeButtonHovered ? kStateHover : kStateIdle);
- for (uint i = 0; i < kNumTabs; ++i) {
- const UIButtonSlot &tab = _uinbData->tabs[i];
- if (!tab.enabled) {
- continue;
- }
+ WidgetState sliderState = kStateIdle;
+ if (_scrollbarDragging) {
+ sliderState = kStatePressed;
+ } else if (_scrollbarHovered) {
+ sliderState = kStateHover;
+ }
+ drawScrollbar(sliderState);
+}
- // Use the active state's sprite for the selected tab, idle for
- // the rest.
- const Common::Rect src = ((int)i == _activeTab && !tab.button.sourceRects[2].isEmpty())
- ? tab.button.sourceRects[2]
- : tab.button.sourceRects[0];
- if (src.isEmpty()) {
- continue;
+Common::Rect NotebookPopup::toPopupLocal(const Common::Rect &chunkRect, bool useGameFrame) const {
+ // Build the element's absolute screen rect: apply the viewport
+ // offset iff its own `destUsesGameFrameOffset` flag is set, then
+ // subtract the popup's absolute screen position. `_screenPosition`
+ // already includes the popup's own game-frame translation, so this
+ // works correctly regardless of which combination of flags the
+ // chunk uses.
+ Common::Rect r = chunkRect;
+ if (useGameFrame) {
+ const VIEW *view = GetEngineData(VIEW);
+ if (view) {
+ r.translate(view->screenPosition.left, view->screenPosition.top);
}
+ }
+ r.translate(-_screenPosition.left, -_screenPosition.top);
+ return r;
+}
+
+Common::Point NotebookPopup::popupLocalMouse(const Common::Point &screenMouse) const {
+ return Common::Point(screenMouse.x - _screenPosition.left,
+ screenMouse.y - _screenPosition.top);
+}
+
+Common::Rect NotebookPopup::computeThumbRect() const {
+ const UISliderRecord &sl = _uinbData->header.slider;
+ if (!_uinbData->header.sliderEnabled || sl.destRect.isEmpty() || sl.sourceRects[0].isEmpty()) {
+ return Common::Rect();
+ }
+
+ const int trackHeight = sl.destRect.height();
+ const int thumbHeight = sl.sourceRects[0].height();
+ const int travel = MAX(0, trackHeight - thumbHeight);
+ const int thumbY = sl.destRect.top + (int)(_scrollPos * travel);
+
+ Common::Rect chunkThumb(sl.destRect.left, thumbY,
+ sl.destRect.left + sl.sourceRects[0].width(),
+ thumbY + thumbHeight);
+ return toPopupLocal(chunkThumb, sl.destUsesGameFrameOffset != 0);
+}
- _drawSurface.blitFrom(_overlayImage, src,
- Common::Point(tab.button.destRect.left - chunkOrigin.x,
- tab.button.destRect.top - chunkOrigin.y));
+void NotebookPopup::drawScrollbar(WidgetState state) {
+ const UISliderRecord &sl = _uinbData->header.slider;
+ if (!_uinbData->header.sliderEnabled)
+ return;
+
+ Common::Rect spr = sl.sourceRects[state];
+ const Common::Rect thumb = computeThumbRect();
+ if (thumb.isEmpty())
+ return;
+
+ _drawSurface.blitFrom(_overlayImage, spr, Common::Point(thumb.left, thumb.top));
+}
+
+void NotebookPopup::drawCloseButton(WidgetState state) {
+ const UIButtonRecord &btn = _uinbData->header.secondaryButton;
+ if (!_uinbData->header.secondaryButtonEnabled || btn.destRect.isEmpty())
+ return;
+
+ Common::Rect spr = btn.sourceRects[state];
+ const Common::Rect dstLocal = toPopupLocal(btn.destRect, btn.destUsesGameFrameOffset != 0);
+
+ const Graphics::ManagedSurface &srcSurf = _closeButtonImage.w != 0 ? _closeButtonImage : _overlayImage;
+ _drawSurface.blitFrom(srcSurf, spr, Common::Point(dstLocal.left, dstLocal.top));
+}
+
+void NotebookPopup::drawTabs() {
+ for (uint i = 0; i < kNumTabs; ++i) {
+ drawTab(i);
+ }
+
+ _needsRedraw = true;
+}
+
+void NotebookPopup::drawTab(uint index, bool drawHover) {
+ const UIButtonSlot &tab = _uinbData->tabs[index];
+ if (!tab.enabled)
+ return;
+
+ WidgetState stateIdx = ((int)index == _activeTab) ? kStatePressed : kStateIdle;
+ if (drawHover)
+ stateIdx = kStateHover;
+ Common::Rect src = tab.button.sourceRects[stateIdx];
+ if (src.isEmpty()) {
+ src = tab.button.sourceRects[kStatePressed];
}
+ if (src.isEmpty())
+ return;
+ const Common::Rect dstLocal = toPopupLocal(tab.button.destRect,
+ tab.button.destUsesGameFrameOffset != 0);
+ _drawSurface.blitFrom(_overlayImage, src,
+ Common::Point(dstLocal.left, dstLocal.top));
_needsRedraw = true;
}
void NotebookPopup::handleInput(NancyInput &input) {
- if (!_isOpen) {
+ if (!_isOpen)
return;
+
+ const Common::Point localMouse = popupLocalMouse(input.mousePos);
+
+ // Scrollbar interaction takes priority while dragging.
+ const UISliderRecord &slider = _uinbData->header.slider;
+ if (_uinbData->header.sliderEnabled) {
+ const Common::Rect trackLocal = toPopupLocal(slider.destRect, slider.destUsesGameFrameOffset != 0);
+ const int trackHeight = trackLocal.height();
+ const int thumbHeight = slider.sourceRects[0].height();
+ const int travel = MAX(0, trackHeight - thumbHeight);
+ const int thumbY = trackLocal.top + (int)(_scrollPos * travel);
+ Common::Rect thumbLocal(trackLocal.left, thumbY,
+ trackLocal.left + slider.sourceRects[0].width(),
+ thumbY + thumbHeight);
+
+ const bool overThumb = thumbLocal.contains(localMouse);
+
+ if (_scrollbarDragging) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+ const int newThumbTop = localMouse.y - _scrollbarGrabOffset;
+ const int clamped = CLIP<int>(newThumbTop, trackLocal.top, trackLocal.top + travel);
+ _scrollPos = travel > 0 ? (float)(clamped - trackLocal.top) / (float)travel : 0.0f;
+ refreshContent();
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _scrollbarDragging = false;
+ drawScrollbar(overThumb ? kStateHover : kStateIdle);
+ _needsRedraw = true;
+ }
+ input.eatMouseInput();
+ return;
+ }
+
+ if (overThumb != _scrollbarHovered) {
+ _scrollbarHovered = overThumb;
+ drawScrollbar(overThumb ? kStateHover : kStateIdle);
+ _needsRedraw = true;
+ }
+ if (overThumb) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (slider.isDraggable && (input.input & NancyInput::kLeftMouseButtonDown)) {
+ _scrollbarDragging = true;
+ _scrollbarGrabOffset = localMouse.y - thumbY;
+ drawScrollbar(kStatePressed);
+ _needsRedraw = true;
+ input.eatMouseInput();
+ return;
+ }
+ }
}
- // Bring the mouse into the chunk's coordinate system so hit-tests
- // against tab destRects work after the popup was translated.
- const Common::Point chunkMouse(
- input.mousePos.x - _screenPosition.left + _uinbData->header.normalDestRect.left,
- input.mousePos.y - _screenPosition.top + _uinbData->header.normalDestRect.top);
+ // Close (X) button takes priority.
+ if (_uinbData->header.secondaryButtonEnabled) {
+ const UIButtonRecord &closeBtn = _uinbData->header.secondaryButton;
+ const Common::Rect closeLocal = toPopupLocal(closeBtn.destRect,
+ closeBtn.destUsesGameFrameOffset != 0);
+ const bool overClose = closeLocal.contains(localMouse);
+ if (overClose != _closeButtonHovered) {
+ _closeButtonHovered = overClose;
+ drawCloseButton(overClose ? kStateHover : kStateIdle);
+ _needsRedraw = true;
+ }
+ if (overClose) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ input.eatMouseInput();
+ close();
+ return;
+ }
+ }
+ }
- // Tab clicks
+ // Tab hover + click. Mirrors the inventory popup's filter-tab
+ // handling so the same enter/exit redraw semantics apply: track
+ // whether any tab is hovered, restore non-hovered tabs to their
+ // idle/active sprite on transitions, and pop the hover sprite for
+ // the one currently under the cursor.
+ const bool wasTabHovered = _tabHovered;
+ _tabHovered = false;
for (uint i = 0; i < kNumTabs; ++i) {
const UIButtonSlot &tab = _uinbData->tabs[i];
- if (!tab.enabled) {
+ if (!tab.enabled)
continue;
+ const Common::Rect tabLocal = toPopupLocal(tab.button.destRect,
+ tab.button.destUsesGameFrameOffset != 0);
+ if (tabLocal.contains(localMouse)) {
+ _tabHovered = true;
+ break;
}
- if (!tab.button.destRect.contains(chunkMouse)) {
+ }
+
+ for (uint i = 0; i < kNumTabs; ++i) {
+ const UIButtonSlot &tab = _uinbData->tabs[i];
+ if (!tab.enabled)
+ continue;
+
+ const Common::Rect tabLocal = toPopupLocal(tab.button.destRect,
+ tab.button.destUsesGameFrameOffset != 0);
+ if (!tabLocal.contains(localMouse)) {
+ // Restore the idle/active sprite when the cursor has just
+ // left this tab (or any tab) so the hover highlight doesn't
+ // linger after the mouse moves away.
+ if (_tabHovered || wasTabHovered)
+ drawTab(i);
continue;
}
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ drawTab(i, true);
if (input.input & NancyInput::kLeftMouseButtonUp) {
if (_activeTab != (int)i) {
_activeTab = (int)i;
+ _scrollPos = 0.0f;
+ _scrollbarDragging = false;
// Play the page-flip sound (first slot of either
// actionable or no-action set; both have 3 alternates).
@@ -176,8 +375,7 @@ void NotebookPopup::handleInput(NancyInput &input) {
g_nancy->_sound->playSound(soundName.toString());
}
- drawBackground();
- drawTabs();
+ refreshContent();
}
input.eatMouseInput();
return;
@@ -191,5 +389,129 @@ void NotebookPopup::handleInput(NancyInput &input) {
}
}
+void NotebookPopup::refreshContent() {
+ // Re-blit the popup background so previous text is wiped, then put
+ // the active tab sprite back on top of it before drawing new text.
+ // Foreground widgets (close button, slider) are painted last so they
+ // always sit visually above the text layer.
+ drawBackground();
+ drawTabs();
+ drawContent();
+ drawForeground();
+}
+
+void NotebookPopup::paintPaperIntoFullSurface() {
+ const Common::Rect &normSrc = _uinbData->header.normalSrcRect;
+ const Common::Rect &normDest = _uinbData->header.normalDestRect;
+ const Common::Rect &chunkTextRect = _uinbData->textRect;
+
+ const int16 paperLeft = normSrc.left + (chunkTextRect.left - normDest.left);
+ const int16 paperTop = normSrc.top + (chunkTextRect.top - normDest.top);
+ const Common::Rect paperSrc(paperLeft, paperTop,
+ paperLeft + chunkTextRect.width(),
+ paperTop + chunkTextRect.height());
+
+ const int stripH = paperSrc.height();
+ if (stripH <= 0)
+ return;
+
+ int y = 0;
+ while (y < (int)_fullSurface.h) {
+ const int rowH = MIN<int>(stripH, (int)_fullSurface.h - y);
+ Common::Rect src = paperSrc;
+ src.bottom = src.top + rowH;
+ _fullSurface.blitFrom(_overlayImage, src, Common::Point(0, y));
+ y += rowH;
+ }
+}
+
+void NotebookPopup::buildTextLines() {
+ if (!_uinbData)
+ return;
+
+ const UIButtonSlot &tab = _uinbData->tabs[_activeTab];
+ if (!tab.enabled)
+ return;
+
+ const uint16 surfaceID = (uint16)tab.id + 2;
+
+ JournalData *journalData = (JournalData *)NancySceneState.getPuzzleData(JournalData::getTag());
+ if (!journalData)
+ return;
+
+ const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
+ if (!autotext)
+ return;
+
+ // Senior-detective Tasks page: hide the to-do list and show the
+ // AUTOTEXT placeholder body instead.
+ if (surfaceID == kNotebookTabTasks && NancySceneState.getDifficulty() == 2) {
+ // TODO: This is specific for Nancy10, adapt it for others, too
+ if (autotext->texts.contains("SHAT70")) {
+ addTextLine(autotext->texts["SHAT70"]);
+ }
+ return;
+ }
+
+ if (!journalData->journalEntries.contains(surfaceID))
+ return;
+
+ const Common::Array<JournalData::Entry> &entries = journalData->journalEntries[surfaceID];
+ for (uint i = 0; i < entries.size(); ++i) {
+ const Common::String &stringID = entries[i].stringID;
+ if (!autotext->texts.contains(stringID))
+ continue;
+
+ Common::String body = autotext->texts[stringID];
+ if (body.empty())
+ continue;
+
+ // Tasks are prefixed with a checkbox showing completion state.
+ // mark % 10 == 8 means "complete".
+ if (surfaceID == kNotebookTabTasks) {
+ const uint16 markStatus = entries[i].mark % 10;
+ body = Common::String(markStatus == 8 ? "<2>" : "<1>") + body;
+ }
+
+ addTextLine(body);
+ }
+}
+
+void NotebookPopup::drawContent() {
+ if (!_uinbData) {
+ return;
+ }
+
+ // textRect from UINB is in chunk coords (relative to normalDestRect);
+ // convert to popup-local for the on-surface blit destination.
+ Common::Rect localTextRect = _uinbData->textRect;
+ localTextRect.translate(-_uinbData->header.normalDestRect.left,
+ -_uinbData->header.normalDestRect.top);
+
+ // Reset HypertextParser state, repaint the paper background under
+ // the text layer, then route content through the shared pipeline.
+ HypertextParser::clear();
+ paintPaperIntoFullSurface();
+ buildTextLines();
+
+ const uint16 fontID = _uinbData->primaryFontID;
+ Common::Rect hypertextBounds(0, 0, _fullSurface.w, _fullSurface.h);
+ drawAllText(hypertextBounds, 0, fontID, fontID);
+
+ // Blit the scrolled vertical slice of the rendered hypertext onto
+ // the popup surface. _drawnTextHeight is the inclusive height of
+ // the rendered content; anything past localTextRect.height() is
+ // reachable via the slider.
+ const int visibleH = localTextRect.height();
+ const int maxScroll = MAX<int>(0, (int)_drawnTextHeight - visibleH);
+ const int scrollY = (int)(_scrollPos * maxScroll);
+ Common::Rect srcSlice(0, scrollY,
+ _fullSurface.w, scrollY + visibleH);
+ _drawSurface.blitFrom(_fullSurface, srcSlice,
+ Common::Point(localTextRect.left, localTextRect.top));
+
+ _needsRedraw = true;
+}
+
} // End of namespace UI
} // End of namespace Nancy
diff --git a/engines/nancy/ui/notebookpopup.h b/engines/nancy/ui/notebookpopup.h
index cd2158a8c14..0e14b2c0c54 100644
--- a/engines/nancy/ui/notebookpopup.h
+++ b/engines/nancy/ui/notebookpopup.h
@@ -23,6 +23,7 @@
#define NANCY_UI_NOTEBOOKPOPUP_H
#include "engines/nancy/renderobject.h"
+#include "engines/nancy/misc/hypertext.h"
namespace Nancy {
@@ -33,7 +34,7 @@ namespace UI {
// Nancy 10+ notebook popup. Driven by the UINB chunk: overlay image + two
// tab buttons.
-class NotebookPopup : public RenderObject {
+class NotebookPopup : public RenderObject, public Misc::HypertextParser {
public:
NotebookPopup();
~NotebookPopup() override = default;
@@ -47,18 +48,65 @@ public:
void close();
void toggle() { if (_isOpen) close(); else open(); }
+ // Re-render the active tab's text content into the text rect.
+ // Called automatically on open() and on tab switch; Scene also
+ // invokes it after a ModifyListEntry AR runs while the popup is open.
+ void refreshContent();
+
private:
static const uint kNumTabs = 2;
+ enum WidgetState {
+ kStatePressed = 0,
+ kStateHover = 1,
+ kStateIdle = 2,
+ kStateDisabled = 3
+ };
+
void drawBackground();
void drawTabs();
+ void drawTab(uint index, bool drawHover = false);
+ void drawContent();
+ // Paint foreground widgets (close button, scrollbar) on top of the
+ // already-drawn background + content layers.
+ void drawForeground();
+ void drawCloseButton(WidgetState state);
+ void drawScrollbar(WidgetState state);
+
+ // Returns the on-popup-surface bounding rect of the slider thumb at
+ // the current scroll position (in popup-local coords).
+ Common::Rect computeThumbRect() const;
+
+ // Convert a chunk-space destRect into popup-local coordinates.
+ Common::Rect toPopupLocal(const Common::Rect &chunkRect, bool useGameFrame) const;
+ Common::Point popupLocalMouse(const Common::Point &screenMouse) const;
+
+ // Populate HypertextParser's text-line list with the active tab's
+ // entries.
+ void buildTextLines();
+
+ void paintPaperIntoFullSurface();
const UINB *_uinbData;
- Graphics::ManagedSurface _overlayImage; // popup background image
+ Graphics::ManagedSurface _overlayImage; // popup background image
+ Graphics::ManagedSurface _closeButtonImage; // header.secondaryButton.primaryImageName
bool _isOpen;
+ bool _closeButtonHovered = false;
+ bool _tabHovered = false;
int _activeTab; // 0..1, matching UINB::tabs index
+
+ // Scrollbar state. Driven by header.slider.
+ float _scrollPos = 0.0f; // [0, 1]: 0 = top, 1 = bottom
+ bool _scrollbarDragging = false;
+ bool _scrollbarHovered = false;
+ int _scrollbarGrabOffset = 0;
+
+ enum NotebookTab {
+ kNotebookTabJournal = 3,
+ kNotebookTabTasks = 4
+ };
};
} // End of namespace UI
More information about the Scummvm-git-logs
mailing list