[Scummvm-git-logs] scummvm master -> 24ad8f0a0c432ab7182a1621dc1631b31da9a4a7

bluegr noreply at scummvm.org
Tue Sep 8 06:50:08 UTC 2026


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

Summary:
24ad8f0a0c NANCY: NANCY15: Implement the Design Select screen


Commit: 24ad8f0a0c432ab7182a1621dc1631b31da9a4a7
    https://github.com/scummvm/scummvm/commit/24ad8f0a0c432ab7182a1621dc1631b31da9a4a7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-08T09:50:00+03:00

Commit Message:
NANCY: NANCY15: Implement the Design Select screen

Changed paths:
  A engines/nancy/state/designselect.cpp
  A engines/nancy/state/designselect.h
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h
    engines/nancy/module.mk
    engines/nancy/nancy.cpp


diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index 3c91921bd37..2e00664e693 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -1447,13 +1447,9 @@ LDSN::LDSN(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	readFilename(*chunkStream, backgroundImageName);
 	readFilename(*chunkStream, overlayImageName);
 
-	// The accept button's "PLAYERCHAR DOWN" and "PLAYERCHAR HILITE" sprites
-	readRect(*chunkStream, acceptDownSrc);
-	readRect(*chunkStream, acceptDownDest);
-	readRect(*chunkStream, acceptHighlightSrc);
-	readRect(*chunkStream, acceptHighlightDest);
-
-	readRectArray(*chunkStream, buttonHotspots, kNumButtons);
+	readRectArray(*chunkStream, buttonDownSrcs, kNumButtons);
+	readRectArray(*chunkStream, buttonHighlightSrcs, kNumButtons);
+	readRectArray(*chunkStream, buttonDests, kNumButtons);
 	readRectArray(*chunkStream, designRowDests, kNumDesignRows);
 
 	fontID = chunkStream->readSint16LE();
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index df7173b8e38..e4751f05f81 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -921,17 +921,15 @@ struct LDSN : public EngineData {
 
 	LDSN(Common::SeekableReadStream *chunkStream);
 
-	Common::Path backgroundImageName;			// "UI_DesignSelectBG"
-	Common::Path overlayImageName;				// "UI_DesignSelect_OVL"
+	Common::Path backgroundImageName;	// "UI_DesignSelectBG", fills the screen
+	Common::Path overlayImageName;		// "UI_DesignSelect_OVL", the buttons' sprite sheet
 
-	// The accept button's two states, drawn out of the background image
-	Common::Rect acceptDownSrc;
-	Common::Rect acceptDownDest;
-	Common::Rect acceptHighlightSrc;
-	Common::Rect acceptHighlightDest;
+	// The two buttons' artwork, cut from the overlay image. [0] applies the
+	// highlighted design, [1] leaves without applying.
+	Common::Array<Common::Rect> buttonDownSrcs;
+	Common::Array<Common::Rect> buttonHighlightSrcs;
+	Common::Array<Common::Rect> buttonDests;
 
-	// Hotspots: [0] accepts the highlighted design, [1] leaves without applying
-	Common::Array<Common::Rect> buttonHotspots;
 	Common::Array<Common::Rect> designRowDests;	// where each design's name is drawn
 	int16 fontID = 0;							// design names
 	int16 highlightFontID = 0;					// ...and the selected one
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 691c668016b..eafd8074d97 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -99,6 +99,7 @@ MODULE_OBJS = \
   ui/textbox.o \
   ui/viewport.o \
   state/credits.o \
+  state/designselect.o \
   state/logo.o \
   state/loadsave.o \
   state/help.o \
diff --git a/engines/nancy/nancy.cpp b/engines/nancy/nancy.cpp
index 2e3516694d1..085b449a151 100644
--- a/engines/nancy/nancy.cpp
+++ b/engines/nancy/nancy.cpp
@@ -49,6 +49,7 @@
 #include "engines/nancy/state/credits.h"
 #include "engines/nancy/state/mainmenu.h"
 #include "engines/nancy/state/setupmenu.h"
+#include "engines/nancy/state/designselect.h"
 #include "engines/nancy/state/loadsave.h"
 #include "engines/nancy/state/savedialog.h"
 
@@ -87,6 +88,7 @@ NancyEngine::~NancyEngine() {
 	destroyState(NancyState::kScene);
 	destroyState(NancyState::kMainMenu);
 	destroyState(NancyState::kSetup);
+	destroyState(NancyState::kDesignSelect);
 	destroyState(NancyState::kLoadSave);
 	destroyState(NancyState::kSaveDialog);
 
@@ -779,6 +781,8 @@ State::State *NancyEngine::getStateObject(NancyState::NancyState state) const {
 		return &State::Map::instance();
 	case NancyState::kSetup:
 		return &State::SetupMenu::instance();
+	case NancyState::kDesignSelect:
+		return &State::DesignSelect::instance();
 	case NancyState::kHelp:
 		return &State::Help::instance();
 	case NancyState::kScene:
@@ -830,6 +834,12 @@ void NancyEngine::destroyState(NancyState::NancyState state) const {
 		if (State::SetupMenu::hasInstance()) {
 			State::SetupMenu::instance().destroy();
 		}
+
+		break;
+	case NancyState::kDesignSelect:
+		if (State::DesignSelect::hasInstance()) {
+			State::DesignSelect::instance().destroy();
+		}
 		break;
 	case NancyState::kLoadSave:
 		if (State::LoadSaveMenu::hasInstance()) {
diff --git a/engines/nancy/state/designselect.cpp b/engines/nancy/state/designselect.cpp
new file mode 100644
index 00000000000..89f1b81a01b
--- /dev/null
+++ b/engines/nancy/state/designselect.cpp
@@ -0,0 +1,297 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/cursor.h"
+#include "engines/nancy/font.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/iff.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+
+#include "engines/nancy/state/designselect.h"
+#include "engines/nancy/state/scene.h"
+
+namespace Common {
+DECLARE_SINGLETON(Nancy::State::DesignSelect);
+}
+
+namespace Nancy {
+namespace State {
+
+// The swatch sits to the right of the design's name, hanging slightly below it
+static const int16 kSwatchOffsetX = 36;
+static const int16 kSwatchOffsetY = 6;
+
+void DesignSelect::process() {
+	switch (_state) {
+	case kInit:
+		init();
+		// fall through
+	case kRun:
+		run();
+		break;
+	case kStop:
+		stop();
+		break;
+	}
+}
+
+void DesignSelect::onStateEnter(const NancyState::NancyState prevState) {
+	registerGraphics();
+}
+
+bool DesignSelect::onStateExit(const NancyState::NancyState nextState) {
+	return true;
+}
+
+void DesignSelect::registerGraphics() {
+	_background.registerGraphics();
+	_list.registerGraphics();
+
+	if (_acceptButton) {
+		_acceptButton->registerGraphics();
+	}
+
+	if (_cancelButton) {
+		_cancelButton->registerGraphics();
+	}
+}
+
+void DesignSelect::buildDesignList() {
+	_designs.clear();
+
+	const PCUI *pcui = GetEngineData(PCUI);
+	if (!pcui || pcui->characters.empty()) {
+		return;
+	}
+
+	// The original enumerates the game directory's .DAT files and keeps the ones
+	// whose name contains the FIRST character's base name, which is why only
+	// Nancy ever has a choice of designs
+	const Common::String &baseName = pcui->characters[0].imageName;
+
+	Common::ArchiveMemberList members;
+	SearchMan.listMatchingMembers(members, Common::Path(baseName + "*.dat"));
+
+	for (const auto &member : members) {
+		Common::String treeName = member->getName();
+		const uint32 dot = treeName.findLastOf('.');
+		if (dot == Common::String::npos) {
+			continue;
+		}
+
+		treeName.erase(dot);
+
+		// Each design's label and swatch come from the PUIH chunk at the head
+		// of its own boot IFF
+		if (!g_nancy->_resource->readCifTree(treeName, "dat", 1)) {
+			continue;
+		}
+
+		IFF *iff = g_nancy->_resource->loadIFF(Common::Path(treeName + "_boot"));
+		if (!iff) {
+			continue;
+		}
+
+		Common::SeekableReadStream *chunkStream = iff->getChunkStream("PUIH");
+		if (chunkStream) {
+			PUIH header(chunkStream);
+			_designs.push_back(Design());
+			Design &design = _designs.back();
+			design.name = treeName;
+			design.themeName = header.themeName;
+			design.swatchImageName = header.swatchImageName;
+
+			delete chunkStream;
+		}
+
+		delete iff;
+
+		if (_designs.size() >= _designData->designRowDests.size()) {
+			// The screen has a fixed number of rows to lay these out in
+			break;
+		}
+	}
+
+	// The design already in use starts out selected
+	const Common::String current = g_nancy->getPlayerCharacterDesign(0);
+	for (uint i = 0; i < _designs.size(); ++i) {
+		Design &design = _designs[i];
+		design.labelDest = _designData->designRowDests[i];
+
+		Graphics::ManagedSurface swatch;
+		if (g_nancy->_resource->loadImage(design.swatchImageName, swatch, design.name)) {
+			design.swatchDest = Common::Rect(swatch.w, swatch.h);
+			design.swatchDest.moveTo(design.labelDest.right + kSwatchOffsetX,
+									design.labelDest.bottom + kSwatchOffsetY - swatch.h);
+
+			// Swatches never change, so they become part of the backdrop the
+			// labels are drawn over
+			_listBase.blitFrom(swatch, design.swatchDest.origin());
+		}
+
+		if (design.name.equalsIgnoreCase(current)) {
+			_selected = i;
+		}
+	}
+
+	if (_selected < 0 && !_designs.empty()) {
+		_selected = 0;
+	}
+}
+
+void DesignSelect::drawDesigns() {
+	const Font *font = g_nancy->_graphics->getFont(_designData->fontID);
+	const Font *highlightFont = g_nancy->_graphics->getFont(_designData->highlightFontID);
+	if (!font || !_list._drawSurface.getPixels()) {
+		return;
+	}
+
+	// Start from the swatches alone, so the previous selection's highlight goes away
+	_list._drawSurface.blitFrom(_listBase);
+
+	for (uint i = 0; i < _designs.size(); ++i) {
+		const Design &design = _designs[i];
+		const Font *rowFont = ((int)i == _selected && highlightFont) ? highlightFont : font;
+
+		// The original anchors its text on the bottom row of the glyphs, while
+		// drawString takes the top of the line
+		rowFont->drawString(&_list._drawSurface, design.themeName,
+			design.labelDest.left, design.labelDest.bottom - rowFont->getFontHeight(),
+			_list._drawSurface.w - design.labelDest.left, 0);
+	}
+
+	_list.setVisible(true);
+	_list.setNeedsRedraw(true);
+}
+
+int DesignSelect::designAt(const Common::Point &mousePos) const {
+	for (uint i = 0; i < _designs.size(); ++i) {
+		if (_designs[i].labelDest.contains(mousePos) || _designs[i].swatchDest.contains(mousePos)) {
+			return i;
+		}
+	}
+
+	return -1;
+}
+
+void DesignSelect::init() {
+	_designData = GetEngineData(LDSN);
+	assert(_designData);
+
+	_background.init(_designData->backgroundImageName);
+	_background.registerGraphics();
+
+	// The overlay is a small sprite sheet holding the buttons' pressed and
+	// highlighted artwork, not a layer of its own
+	g_nancy->_resource->loadImage(_designData->overlayImageName, _buttonSprites);
+
+	// The design list is drawn over the backdrop, so it covers the whole screen
+	const uint16 width = MAX<uint16>(_background._drawSurface.w, 1);
+	const uint16 height = MAX<uint16>(_background._drawSurface.h, 1);
+	const uint32 transColor = g_nancy->_graphics->getTransColor();
+
+	_listBase.create(width, height, g_nancy->_graphics->getInputPixelFormat());
+	_listBase.clear(transColor);
+	_list._drawSurface.create(width, height, _listBase.format);
+	_list._drawSurface.setTransparentColor(transColor);
+	_list.moveTo(Common::Rect(width, height));
+	_list.setTransparent(true);
+
+	if (_buttonSprites.getPixels() &&
+			_designData->buttonDests.size() >= LDSN::kNumButtons &&
+			_designData->buttonDownSrcs.size() >= LDSN::kNumButtons &&
+			_designData->buttonHighlightSrcs.size() >= LDSN::kNumButtons) {
+		_acceptButton.reset(new UI::Button(5, _buttonSprites, _designData->buttonDownSrcs[0],
+			_designData->buttonDests[0], _designData->buttonHighlightSrcs[0]));
+		_acceptButton->init();
+		_acceptButton->setVisible(false);
+
+		_cancelButton.reset(new UI::Button(5, _buttonSprites, _designData->buttonDownSrcs[1],
+			_designData->buttonDests[1], _designData->buttonHighlightSrcs[1]));
+		_cancelButton->init();
+		_cancelButton->setVisible(false);
+	}
+
+	buildDesignList();
+	drawDesigns();
+
+	g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+	g_nancy->setMouseEnabled(true);
+
+	registerGraphics();
+
+	_state = kRun;
+}
+
+void DesignSelect::run() {
+	NancyInput input = g_nancy->_input->getInput();
+
+	if (_acceptButton) {
+		_acceptButton->handleInput(input);
+
+		if (_acceptButton->_isClicked) {
+			g_nancy->_sound->playSound("BUOK");
+			_accepted = true;
+			_state = kStop;
+			return;
+		}
+	}
+
+	if (_cancelButton) {
+		_cancelButton->handleInput(input);
+
+		if (_cancelButton->_isClicked) {
+			g_nancy->_sound->playSound("BUOK");
+			_state = kStop;
+			return;
+		}
+	}
+
+	const int hovered = designAt(input.mousePos);
+	if (hovered >= 0 && (input.input & NancyInput::kLeftMouseButtonUp) && hovered != _selected) {
+		_selected = hovered;
+		drawDesigns();
+	}
+
+	g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+}
+
+void DesignSelect::stop() {
+	if (g_nancy->_sound->isSoundPlaying("BUOK")) {
+		return;
+	}
+
+	if (_accepted && _selected >= 0) {
+		// Only the choice is recorded here; the scene rebuilds its UI from it
+		// once the player is back in the game
+		NancySceneState.setPlayerCharacterDesign(0, _designs[_selected].name);
+	}
+
+	// Hand the setup menu back the state it was opened from, so closing it in
+	// turn returns to the game rather than back here
+	g_nancy->setState(NancyState::kSetup, g_nancy->getPreviousState());
+}
+
+} // End of namespace State
+} // End of namespace Nancy
diff --git a/engines/nancy/state/designselect.h b/engines/nancy/state/designselect.h
new file mode 100644
index 00000000000..430f08af6e7
--- /dev/null
+++ b/engines/nancy/state/designselect.h
@@ -0,0 +1,98 @@
+/* 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 NANCY_STATE_DESIGNSELECT_H
+#define NANCY_STATE_DESIGNSELECT_H
+
+#include "common/ptr.h"
+#include "common/singleton.h"
+
+#include "engines/nancy/state/state.h"
+#include "engines/nancy/renderobject.h"
+#include "engines/nancy/ui/fullscreenimage.h"
+#include "engines/nancy/ui/button.h"
+
+namespace Nancy {
+
+struct LDSN;
+
+namespace State {
+
+// Nancy15 "Design Select": picks the look Nancy's UI wears. Every design is a
+// CIF tree of its own (PUI_CRE_Nancy_Default, _Jungle, _Pink_Hibiscus, ...);
+// the screen lists whichever ones ship with the game and hands the chosen one
+// to Scene::changePlayerCharacterDesign(). Opened from the setup menu.
+class DesignSelect : public State, public Common::Singleton<DesignSelect> {
+public:
+	// State API
+	void process() override;
+	void onStateEnter(const NancyState::NancyState prevState) override;
+	bool onStateExit(const NancyState::NancyState nextState) override;
+
+private:
+	// One selectable look, described by the PUIH chunk of its own boot IFF
+	struct Design {
+		Common::String name;			// CIF tree name, e.g. "PUI_CRE_Nancy_Jungle"
+		Common::String themeName;		// label, e.g. "Nancy Classic Look (Default)"
+		Common::Path swatchImageName;	// thumbnail shown next to the label
+		Common::Rect labelDest;
+		Common::Rect swatchDest;
+	};
+
+	enum State { kInit, kRun, kStop };
+
+	void init();
+	void run();
+	void stop();
+
+	void registerGraphics();
+
+	// Finds every design tree belonging to the active character and reads its
+	// PUIH chunk for the label and swatch
+	void buildDesignList();
+	void drawDesigns();
+
+	// Index of the design row under the given point, or -1
+	int designAt(const Common::Point &mousePos) const;
+
+	UI::FullScreenImage _background;
+	// The design names and swatches, drawn straight onto the backdrop
+	RenderObject _list { 6 };
+	// The swatches on their own; the names are drawn over a fresh copy of this
+	// whenever the selection moves
+	Graphics::ManagedSurface _listBase;
+	// Sprite sheet the two buttons' artwork is cut from
+	Graphics::ManagedSurface _buttonSprites;
+	Common::ScopedPtr<UI::Button> _acceptButton;
+	Common::ScopedPtr<UI::Button> _cancelButton;
+
+	Common::Array<Design> _designs;
+	int _selected = -1;
+	bool _accepted = false;
+
+	State _state = kInit;
+	const LDSN *_designData = nullptr;
+};
+
+} // End of namespace State
+} // End of namespace Nancy
+
+#endif // NANCY_STATE_DESIGNSELECT_H




More information about the Scummvm-git-logs mailing list