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

neuromancer noreply at scummvm.org
Wed Mar 25 08:23:33 UTC 2026


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

Summary:
4bb2459153 FREESCAPE: make sure colors changes per area in the ui of castle cpc
94076fb676 FREESCAPE: riddle frame rendering for castle cpc
562a7df544 FREESCAPE: riddle frame rendering for castle amiga
cc2cc2bb24 FREESCAPE: load health indicator in castle amiga
fd609715b8 FREESCAPE: loaded more frames in castle amiga, including cursors
c6e7608745 FREESCAPE: added pulsating surface detection and rendering for dark and castle (amiga/atari)
e4f5bc30f9 FREESCAPE: added an option for WASD movement and shift for run


Commit: 4bb2459153f77a71bd8bfb17a4cee2aa82790114
    https://github.com/scummvm/scummvm/commit/4bb2459153f77a71bd8bfb17a4cee2aa82790114
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:15+01:00

Commit Message:
FREESCAPE: make sure colors changes per area in the ui of castle cpc

Changed paths:
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/castle.h
    engines/freescape/games/castle/cpc.cpp


diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index e6b202b7f94..0bb5fbb4bc7 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -83,6 +83,11 @@ CastleEngine::CastleEngine(OSystem *syst, const ADGameDescription *gd) : Freesca
 	_spiritsMeterIndicatorSideFrame = nullptr;
 	_strenghtBackgroundFrame = nullptr;
 	_strenghtBarFrame = nullptr;
+	_strenghtBackgroundCLUT8 = nullptr;
+	_strenghtBarCLUT8 = nullptr;
+	_spiritsMeterBgCLUT8 = nullptr;
+	_spiritsMeterIndCLUT8 = nullptr;
+	_keysBorderCLUT8 = nullptr;
 	_menu = nullptr;
 	_menuButtons = nullptr;
 
@@ -328,6 +333,120 @@ Common::Array<Graphics::ManagedSurface *> CastleEngine::loadFramesWithHeaderCPC(
 	return frames;
 }
 
+void CastleEngine::convertCPCSprite(Graphics::ManagedSurface *clut8, Graphics::ManagedSurface *&argb, bool transparentInk0) {
+	if (argb) {
+		argb->free();
+		delete argb;
+	}
+	if (transparentInk0) {
+		// Ink 0 = value 0 (transparent for copyRectToSurfaceWithKey with back=0)
+		argb = new Graphics::ManagedSurface();
+		argb->create(clut8->w, clut8->h, _gfx->_texturePixelFormat);
+		argb->fillRect(Common::Rect(0, 0, clut8->w, clut8->h), 0);
+
+		byte palette[4 * 3];
+		clut8->grabPalette(palette, 0, 4);
+
+		for (int y = 0; y < clut8->h; y++) {
+			for (int x = 0; x < clut8->w; x++) {
+				byte idx = clut8->getPixel(x, y);
+				if (idx != 0) {
+					uint32 color = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
+						palette[idx * 3], palette[idx * 3 + 1], palette[idx * 3 + 2]);
+					argb->setPixel(x, y, color);
+				}
+			}
+		}
+	} else {
+		// Opaque: ink 0 = solid black, fully covers what's beneath
+		Graphics::Surface *converted = _gfx->convertImageFormatIfNecessary(clut8);
+		argb = new Graphics::ManagedSurface();
+		argb->copyFrom(*converted);
+		converted->free();
+		delete converted;
+	}
+}
+
+Graphics::ManagedSurface *CastleEngine::loadFrameWithHeaderCPCIndexed(Common::SeekableReadStream *file, int pos) {
+	file->seek(pos);
+	int w = file->readByte();
+	int h = file->readByte();
+	file->readByte(); // mask
+	file->readUint16LE(); // frameSize
+	Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+	surface->create(w * 4, h, Graphics::PixelFormat::createFormatCLUT8());
+	surface->fillRect(Common::Rect(0, 0, w * 4, h), 0);
+	for (int y = 0; y < h; y++)
+		for (int col = 0; col < w; col++) {
+			byte cpc_byte = file->readByte();
+			for (int i = 0; i < 4; i++)
+				surface->setPixel(col * 4 + i, y, getCPCPixel(cpc_byte, i, true));
+		}
+	return surface;
+}
+
+Common::Array<Graphics::ManagedSurface *> CastleEngine::loadFramesWithHeaderCPCIndexed(Common::SeekableReadStream *file, int pos, int numFrames) {
+	file->seek(pos);
+	int w = file->readByte();
+	int h = file->readByte();
+	file->readByte(); // mask
+	file->readUint16LE(); // frameSize
+	Common::Array<Graphics::ManagedSurface *> frames;
+	for (int f = 0; f < numFrames; f++) {
+		Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+		surface->create(w * 4, h, Graphics::PixelFormat::createFormatCLUT8());
+		surface->fillRect(Common::Rect(0, 0, w * 4, h), 0);
+		for (int y = 0; y < h; y++)
+			for (int col = 0; col < w; col++) {
+				byte cpc_byte = file->readByte();
+				for (int i = 0; i < 4; i++)
+					surface->setPixel(col * 4 + i, y, getCPCPixel(cpc_byte, i, true));
+			}
+		frames.push_back(surface);
+	}
+	return frames;
+}
+
+void CastleEngine::updateCPCSpritesPalette() {
+	byte palette[4 * 3];
+	for (int c = 0; c < 4; c++) {
+		uint8 r, g, b;
+		_gfx->selectColorFromFourColorPalette(c, r, g, b);
+		palette[c * 3 + 0] = r;
+		palette[c * 3 + 1] = g;
+		palette[c * 3 + 2] = b;
+	}
+
+	if (_keysBorderCLUT8) {
+		_keysBorderCLUT8->setPalette(palette, 0, 4);
+		convertCPCSprite(_keysBorderCLUT8, _keysBorderFrames[0], true);
+	}
+	if (_spiritsMeterBgCLUT8) {
+		_spiritsMeterBgCLUT8->setPalette(palette, 0, 4);
+		convertCPCSprite(_spiritsMeterBgCLUT8, _spiritsMeterIndicatorBackgroundFrame);
+	}
+	if (_spiritsMeterIndCLUT8) {
+		_spiritsMeterIndCLUT8->setPalette(palette, 0, 4);
+		convertCPCSprite(_spiritsMeterIndCLUT8, _spiritsMeterIndicatorFrame, true);
+	}
+	if (_strenghtBackgroundCLUT8) {
+		_strenghtBackgroundCLUT8->setPalette(palette, 0, 4);
+		convertCPCSprite(_strenghtBackgroundCLUT8, _strenghtBackgroundFrame);
+	}
+	if (_strenghtBarCLUT8) {
+		_strenghtBarCLUT8->setPalette(palette, 0, 4);
+		convertCPCSprite(_strenghtBarCLUT8, _strenghtBarFrame);
+	}
+	for (int f = 0; f < (int)_strenghtWeightsCLUT8.size(); f++) {
+		_strenghtWeightsCLUT8[f]->setPalette(palette, 0, 4);
+		convertCPCSprite(_strenghtWeightsCLUT8[f], _strenghtWeightsFrames[f], true);
+	}
+	for (int f = 0; f < (int)_flagCLUT8.size(); f++) {
+		_flagCLUT8[f]->setPalette(palette, 0, 4);
+		convertCPCSprite(_flagCLUT8[f], _flagFrames[f]);
+	}
+}
+
 Graphics::ManagedSurface *CastleEngine::loadFrameCPC(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, const uint32 *cpcPalette) {
 	for (int y = 0; y < height; y++) {
 		for (int col = 0; col < width; col++) {
@@ -487,6 +606,10 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
 	_gfx->clearColorPairArray();
 
 	swapPalette(areaID);
+
+	if (isCPC())
+		updateCPCSpritesPalette();
+
 	if (isDOS()) {
 		_gfx->_colorPair[_currentArea->_underFireBackgroundColor] = _currentArea->_extraColor[1];
 		_gfx->_colorPair[_currentArea->_usualBackgroundColor] = _currentArea->_extraColor[0];
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 97261220129..bea91416378 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -136,6 +136,21 @@ public:
 	Graphics::ManagedSurface *_endGameBackgroundFrame;
 	Graphics::ManagedSurface *_gameOverBackgroundFrame;
 
+	// CPC: CLUT8 versions of UI sprites (indexed by ink 0-3). On area change,
+	// we setPalette + convert to ARGB, like the border does in swapPalette.
+	Graphics::ManagedSurface *_strenghtBackgroundCLUT8;
+	Graphics::ManagedSurface *_strenghtBarCLUT8;
+	Common::Array<Graphics::ManagedSurface *> _strenghtWeightsCLUT8;
+	Graphics::ManagedSurface *_spiritsMeterBgCLUT8;
+	Graphics::ManagedSurface *_spiritsMeterIndCLUT8;
+	Graphics::ManagedSurface *_keysBorderCLUT8;
+	Common::Array<Graphics::ManagedSurface *> _flagCLUT8;
+	uint32 _cpcUIPalette[4]; // used by gate rendering
+	void convertCPCSprite(Graphics::ManagedSurface *clut8, Graphics::ManagedSurface *&argb, bool transparentInk0 = false);
+	Graphics::ManagedSurface *loadFrameWithHeaderCPCIndexed(Common::SeekableReadStream *file, int pos);
+	Common::Array<Graphics::ManagedSurface *> loadFramesWithHeaderCPCIndexed(Common::SeekableReadStream *file, int pos, int numFrames);
+	void updateCPCSpritesPalette();
+
 	Common::Array<byte> _modData; // Embedded ProTracker module (Amiga demo)
 	Common::Array<int> _keysCollected;
 	bool _useRockTravel;
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index cd9f8022774..91419e825ee 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -199,38 +199,61 @@ void CastleEngine::loadAssetsCPCFullGame() {
 
 	_background = loadFrame(&mountainsStream, background, backgroundWidth, backgroundHeight, front);
 
-	// CPC UI Sprites - located at different offsets than ZX Spectrum!
-	// CPC uses Mode 1 format (4 pixels per byte, 2 bits per pixel).
-	// Sprite pixel values 0-3 are CPC ink numbers that map to the border palette.
-	uint32 cpcPalette[4];
-	for (int i = 0; i < 4; i++) {
-		cpcPalette[i] = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
-			kCPCPaletteCastleBorderData[i][0],
-			kCPCPaletteCastleBorderData[i][1],
-			kCPCPaletteCastleBorderData[i][2]);
+	// CPC UI Sprites stored as CLUT8 (indexed by ink 0-3).
+	// On real CPC hardware, the 4-color palette changes per area, automatically
+	// recoloring everything. We store CLUT8 sprites and setPalette + convert
+	// when the area changes, just like the border does in swapPalette.
+	_keysBorderCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x2362);
+	_spiritsMeterBgCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x2383);
+	_spiritsMeterIndCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x2408);
+	_strenghtBackgroundCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x242D);
+	_strenghtBarCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x2531);
+	_strenghtWeightsCLUT8 = loadFramesWithHeaderCPCIndexed(&file, 0x2569, 4);
+	_flagCLUT8 = loadFramesWithHeaderCPCIndexed(&file, 0x2654, 4);
+
+	// Set initial border palette, convert to ARGB, and populate the drawing surfaces
+	{
+		byte initPalette[4 * 3];
+		for (int c = 0; c < 4; c++) {
+			initPalette[c * 3 + 0] = kCPCPaletteCastleBorderData[c][0];
+			initPalette[c * 3 + 1] = kCPCPaletteCastleBorderData[c][1];
+			initPalette[c * 3 + 2] = kCPCPaletteCastleBorderData[c][2];
+		}
+		_keysBorderCLUT8->setPalette(initPalette, 0, 4);
+		_spiritsMeterBgCLUT8->setPalette(initPalette, 0, 4);
+		_spiritsMeterIndCLUT8->setPalette(initPalette, 0, 4);
+		_strenghtBackgroundCLUT8->setPalette(initPalette, 0, 4);
+		_strenghtBarCLUT8->setPalette(initPalette, 0, 4);
+		for (auto *s : _strenghtWeightsCLUT8)
+			s->setPalette(initPalette, 0, 4);
+		for (auto *s : _flagCLUT8)
+			s->setPalette(initPalette, 0, 4);
+
+		for (int i = 0; i < 4; i++)
+			_cpcUIPalette[i] = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
+				kCPCPaletteCastleBorderData[i][0],
+				kCPCPaletteCastleBorderData[i][1],
+				kCPCPaletteCastleBorderData[i][2]);
+
+		Graphics::ManagedSurface *tmp = nullptr;
+		convertCPCSprite(_keysBorderCLUT8, tmp, true);
+		_keysBorderFrames.push_back(tmp);
+		convertCPCSprite(_spiritsMeterBgCLUT8, _spiritsMeterIndicatorBackgroundFrame);
+		convertCPCSprite(_spiritsMeterIndCLUT8, _spiritsMeterIndicatorFrame, true);
+		convertCPCSprite(_strenghtBackgroundCLUT8, _strenghtBackgroundFrame);
+		convertCPCSprite(_strenghtBarCLUT8, _strenghtBarFrame);
+		for (int f = 0; f < 4; f++) {
+			tmp = nullptr;
+			convertCPCSprite(_strenghtWeightsCLUT8[f], tmp, true);
+			_strenghtWeightsFrames.push_back(tmp);
+		}
+		for (int f = 0; f < 4; f++) {
+			tmp = nullptr;
+			convertCPCSprite(_flagCLUT8[f], tmp);
+			_flagFrames.push_back(tmp);
+		}
 	}
 
-	// Keys Border: CPC offset 0x2362 (8x14 px, 1 frame - matches ZX key_sprite)
-	_keysBorderFrames.push_back(loadFrameWithHeaderCPC(&file, 0x2362, cpcPalette));
-
-	// Spirit Meter Background: CPC offset 0x2383 (64x8 px - matches ZX spirit_meter_bg)
-	_spiritsMeterIndicatorBackgroundFrame = loadFrameWithHeaderCPC(&file, 0x2383, cpcPalette);
-
-	// Spirit Meter Indicator: CPC offset 0x2408 (16x8 px - matches ZX spirit_meter_indicator)
-	_spiritsMeterIndicatorFrame = loadFrameWithHeaderCPC(&file, 0x2408, cpcPalette);
-
-	// Strength Background: CPC offset 0x242D (68x15 px - matches ZX strength_bg)
-	_strenghtBackgroundFrame = loadFrameWithHeaderCPC(&file, 0x242D, cpcPalette);
-
-	// Strength Bar: CPC offset 0x2531 (68x3 px - matches ZX strength_bar)
-	_strenghtBarFrame = loadFrameWithHeaderCPC(&file, 0x2531, cpcPalette);
-
-	// Strength Weights: CPC offset 0x2569 (4x15 px, 4 frames - matches ZX weight_sprite w=1,h=15)
-	_strenghtWeightsFrames = loadFramesWithHeaderCPC(&file, 0x2569, 4, cpcPalette);
-
-	// Flag Animation: CPC offset 0x2654 (16x9 px, 4 frames)
-	_flagFrames = loadFramesWithHeaderCPC(&file, 0x2654, 4, cpcPalette);
-
 	// Gate image (portcullis) for game start/end animation.
 	// The CPC gate is NOT a pre-rendered bitmap; it is procedurally generated
 	// from small pixel pattern tables stored at file offset 0x75EF-0x764C.
@@ -280,13 +303,13 @@ void CastleEngine::loadAssetsCPCFullGame() {
 					for (int p = 0; p < 2; p++) {
 						int ink = getCPCPixel(kGateLastRow[0], p, true);
 						if (ink)
-							_gameOverBackgroundFrame->setPixel(bx + p, y, cpcPalette[ink]);
+							_gameOverBackgroundFrame->setPixel(bx + p, y, _cpcUIPalette[ink]);
 					}
 					// Right edge byte: pixels 2,3 from pattern
 					for (int p = 2; p < 4; p++) {
 						int ink = getCPCPixel(kGateLastRow[1], p, true);
 						if (ink)
-							_gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, cpcPalette[ink]);
+							_gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, _cpcUIPalette[ink]);
 					}
 				}
 			} else {
@@ -324,7 +347,7 @@ void CastleEngine::loadAssetsCPCFullGame() {
 							for (int p = 0; p < 4; p++) {
 								int ink = getCPCPixel(cpcByte, p, true);
 								if (ink)
-									_gameOverBackgroundFrame->setPixel(bx + bi * 4 + p, y, cpcPalette[ink]);
+									_gameOverBackgroundFrame->setPixel(bx + bi * 4 + p, y, _cpcUIPalette[ink]);
 							}
 						}
 					}
@@ -336,13 +359,13 @@ void CastleEngine::loadAssetsCPCFullGame() {
 						for (int p = 0; p < 2; p++) {
 							int ink = getCPCPixel(kGateInterBar[patIdx][0], p, true);
 							if (ink)
-								_gameOverBackgroundFrame->setPixel(bx + p, y, cpcPalette[ink]);
+								_gameOverBackgroundFrame->setPixel(bx + p, y, _cpcUIPalette[ink]);
 						}
 						// Right edge (byte 5): pixels 2,3 from pattern byte 1
 						for (int p = 2; p < 4; p++) {
 							int ink = getCPCPixel(kGateInterBar[patIdx][1], p, true);
 							if (ink)
-								_gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, cpcPalette[ink]);
+								_gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, _cpcUIPalette[ink]);
 						}
 					}
 				}


Commit: 94076fb6760945ef4446744c522729a72a44b9be
    https://github.com/scummvm/scummvm/commit/94076fb6760945ef4446744c522729a72a44b9be
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00

Commit Message:
FREESCAPE: riddle frame rendering for castle cpc

Changed paths:
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/castle.h
    engines/freescape/games/castle/cpc.cpp


diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 0bb5fbb4bc7..09d12e5b5a0 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -94,6 +94,7 @@ CastleEngine::CastleEngine(OSystem *syst, const ADGameDescription *gd) : Freesca
 	_riddleTopFrame = nullptr;
 	_riddleBottomFrame = nullptr;
 	_riddleBackgroundFrame = nullptr;
+	_riddleNailFrame = nullptr;
 
 	_endGameThroneFrame = nullptr;
 	_endGameBackgroundFrame = nullptr;
@@ -1375,6 +1376,9 @@ void CastleEngine::drawFullscreenRiddleAndWait(uint16 riddle) {
 		case Common::kRenderZX:
 			frontColor = 7;
 			break;
+		case Common::kRenderCPC:
+			frontColor = _gfx->_inkColor;
+			break;
 		default:
 			break;
 	}
@@ -1438,7 +1442,11 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
 	if (isDOS()) {
 		x = 40;
 		y = 34;
-	} else if (isSpectrum() || isCPC()) {
+	} else if (isCPC()) {
+		x = 40;
+		y = 46;
+		maxWidth = 139;
+	} else if (isSpectrum()) {
 		x = 64;
 		y = 37;
 	} else if (isAmiga()) {
@@ -1446,6 +1454,18 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
 		y = 33;
 		maxWidth = 139;
 	}
+	// Draw rope lines and nail above the riddle frame (CPC)
+	if (isCPC() && _riddleNailFrame) {
+		int nailX = x + (_viewArea.width() - _riddleNailFrame->w) / 2;
+		int nailY = _viewArea.top + 2;
+		int nailCenterX = nailX + _riddleNailFrame->w / 2;
+		int nailCenterY = nailY + _riddleNailFrame->h / 2;
+		// Rope lines first, then nail on top
+		surface->drawLine(nailCenterX, nailCenterY, x, y, front);
+		surface->drawLine(nailCenterX, nailCenterY, x + _viewArea.width() - 1, y, front);
+		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_riddleNailFrame, nailX, nailY, Common::Rect(0, 0, _riddleNailFrame->w, _riddleNailFrame->h), 0);
+	}
+
 	// Draw riddle frame borders (if available)
 	if (_riddleTopFrame) {
 		surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, x, y, Common::Rect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h));
@@ -1468,7 +1488,10 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
 	if (isDOS()) {
 		x = 38;
 		y = 33;
-	} else if (isSpectrum() || isCPC()) {
+	} else if (isCPC()) {
+		x = 40;
+		y = 33;
+	} else if (isSpectrum()) {
 		x = 64;
 		y = 36;
 	} else if (isAmiga()) {
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index bea91416378..ba04558213b 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -131,6 +131,7 @@ public:
 	Graphics::ManagedSurface *_riddleTopFrame;
 	Graphics::ManagedSurface *_riddleBackgroundFrame;
 	Graphics::ManagedSurface *_riddleBottomFrame;
+	Graphics::ManagedSurface *_riddleNailFrame;
 
 	Graphics::ManagedSurface *_endGameThroneFrame;
 	Graphics::ManagedSurface *_endGameBackgroundFrame;
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index 91419e825ee..4c92dc765ca 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -117,6 +117,31 @@ byte mountainsData[288] {
 
 
 
+// Expand a 5-byte CPC riddle frame row definition into a 240-pixel CLUT8 row.
+// Format: 2 left border bytes + 1 fill byte (repeated 54×) + 2 right border bytes,
+// padded with 1 black byte on each side.
+void expandRiddleRow(const byte *src, Graphics::ManagedSurface *surface, int y) {
+	int x = 0;
+	for (int p = 0; p < 4; p++)
+		surface->setPixel(x++, y, 0);
+	for (int b = 0; b < 2; b++) {
+		byte cpcByte = src[b];
+		for (int p = 0; p < 4; p++)
+			surface->setPixel(x++, y, getCPCPixel(cpcByte, p, true));
+	}
+	byte fillByte = src[2];
+	for (int b = 0; b < 54; b++)
+		for (int p = 0; p < 4; p++)
+			surface->setPixel(x++, y, getCPCPixel(fillByte, p, true));
+	for (int b = 0; b < 2; b++) {
+		byte cpcByte = src[3 + b];
+		for (int p = 0; p < 4; p++)
+			surface->setPixel(x++, y, getCPCPixel(cpcByte, p, true));
+	}
+	for (int p = 0; p < 4; p++)
+		surface->setPixel(x++, y, 0);
+}
+
 void CastleEngine::loadAssetsCPCFullGame() {
 	Common::File file;
 	uint8 r, g, b;
@@ -254,6 +279,65 @@ void CastleEngine::loadAssetsCPCFullGame() {
 		}
 	}
 
+	// Riddle frame graphics at file offset 0x26E9 (CPC addr 0x31A9).
+	// The CPC draw function expands each 5-byte row to:
+	// 1 black + 2 left border + 54×fill + 2 right border + 1 black = 60 bytes = 240 pixels.
+	// Structure: 7 top rows + 1 body row (repeated) + 7 bottom rows (top reversed).
+	{
+		static const int kRiddleFrameOffset = 0x26E9;
+		static const int kTopRows = 7;
+		static const int kRowWidth = 240; // pixels
+
+		file.seek(kRiddleFrameOffset);
+		byte riddleData[40];
+		file.read(riddleData, 40);
+
+		// Top frame: 7 rows
+		Graphics::ManagedSurface *topCLUT8 = new Graphics::ManagedSurface();
+		topCLUT8->create(kRowWidth, kTopRows, Graphics::PixelFormat::createFormatCLUT8());
+		topCLUT8->fillRect(Common::Rect(0, 0, kRowWidth, kTopRows), 0);
+		for (int row = 0; row < kTopRows; row++)
+			expandRiddleRow(&riddleData[row * 5], topCLUT8, row);
+
+		// Background: 1 row (the body row after the 7 top rows)
+		Graphics::ManagedSurface *bgCLUT8 = new Graphics::ManagedSurface();
+		bgCLUT8->create(kRowWidth, 1, Graphics::PixelFormat::createFormatCLUT8());
+		bgCLUT8->fillRect(Common::Rect(0, 0, kRowWidth, 1), 0);
+		expandRiddleRow(&riddleData[kTopRows * 5], bgCLUT8, 0);
+
+		// Bottom frame: 7 rows (top data in reverse order)
+		Graphics::ManagedSurface *bottomCLUT8 = new Graphics::ManagedSurface();
+		bottomCLUT8->create(kRowWidth, kTopRows, Graphics::PixelFormat::createFormatCLUT8());
+		bottomCLUT8->fillRect(Common::Rect(0, 0, kRowWidth, kTopRows), 0);
+		for (int row = 0; row < kTopRows; row++)
+			expandRiddleRow(&riddleData[(kTopRows - 1 - row) * 5], bottomCLUT8, row);
+
+		// Set palette and convert
+		byte initPalette[4 * 3];
+		for (int c = 0; c < 4; c++) {
+			initPalette[c * 3 + 0] = kCPCPaletteCastleBorderData[c][0];
+			initPalette[c * 3 + 1] = kCPCPaletteCastleBorderData[c][1];
+			initPalette[c * 3 + 2] = kCPCPaletteCastleBorderData[c][2];
+		}
+		topCLUT8->setPalette(initPalette, 0, 4);
+		bgCLUT8->setPalette(initPalette, 0, 4);
+		bottomCLUT8->setPalette(initPalette, 0, 4);
+
+		convertCPCSprite(topCLUT8, _riddleTopFrame);
+		convertCPCSprite(bgCLUT8, _riddleBackgroundFrame);
+		convertCPCSprite(bottomCLUT8, _riddleBottomFrame);
+
+		// Nail sprite at file offset 0x2711 (8×7 pixels, drawn above the frame)
+		Graphics::ManagedSurface *nailCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x2711);
+		nailCLUT8->setPalette(initPalette, 0, 4);
+		convertCPCSprite(nailCLUT8, _riddleNailFrame);
+
+		delete topCLUT8;
+		delete bgCLUT8;
+		delete bottomCLUT8;
+		delete nailCLUT8;
+	}
+
 	// Gate image (portcullis) for game start/end animation.
 	// The CPC gate is NOT a pre-rendered bitmap; it is procedurally generated
 	// from small pixel pattern tables stored at file offset 0x75EF-0x764C.


Commit: 562a7df544b281a1819eda95e79061e829b2544c
    https://github.com/scummvm/scummvm/commit/562a7df544b281a1819eda95e79061e829b2544c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00

Commit Message:
FREESCAPE: riddle frame rendering for castle amiga

Changed paths:
    engines/freescape/games/castle/amiga.cpp
    engines/freescape/games/castle/castle.cpp


diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index e8399d42666..6dc21dd004d 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -138,22 +138,25 @@ void CastleEngine::loadAssetsAmigaDemo() {
 
 	file.seek(0x11eec);
 	Common::Array<Graphics::ManagedSurface *> chars;
+	Common::Array<Graphics::ManagedSurface *> charsRiddle;
 	for (int i = 0; i < 90; i++) {
 		Graphics::ManagedSurface *img = loadFrameFromPlanes(&file, 8, 8);
-		//Graphics::ManagedSurface *imgRiddle = new Graphics::ManagedSurface();
-		//imgRiddle->copyFrom(*img);
+		Graphics::ManagedSurface *imgRiddle = new Graphics::ManagedSurface();
+		imgRiddle->copyFrom(*img);
 
 		chars.push_back(img);
 		chars[i]->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
 
-		//charsRiddle.push_back(imgRiddle);
-		//charsRiddle[i]->convertToInPlace(_gfx->_texturePixelFormat, (byte *)&kEGARiddleFontPalette, 16);
+		charsRiddle.push_back(imgRiddle);
+		charsRiddle[i]->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
 	}
-	// 0x1356c
 
 	_font = Font(chars);
 	_font.setCharWidth(9);
 
+	_fontRiddle = Font(charsRiddle);
+	_fontRiddle.setCharWidth(9);
+
 	load8bitBinary(&file, 0x162a6, 16);
 	for (int i = 0; i < 3; i++) {
 		debugC(1, kFreescapeDebugParser, "Continue to parse area index %d at offset %x", _areaMap.size() + i + 1, (int)file.pos());
@@ -220,15 +223,36 @@ void CastleEngine::loadAssetsAmigaDemo() {
 		_flagFrames.push_back(frame);
 	}
 
+	// Riddle mask (memory 0x3C6DA, file 0x3C6F6): 16 words, one per 16-pixel column.
+	// Applied per-pixel: frame_pixel = (mask_bit == 1) ? frame_pixel : 0.
+	// Same mask for every row. Trims the frame edges for proper compositing.
+	file.seek(0x3c6f6);
+	uint16 riddleMask[16];
+	for (int i = 0; i < 16; i++)
+		riddleMask[i] = file.readUint16BE();
+
 	// Riddle frames (memory 0x3C6FA: top 20 rows + bg 1 row + bottom 8 rows, 256px wide)
 	file.seek(0x3c716);
 	_riddleTopFrame = loadFrameFromPlanesInterleaved(&file, 16, 20);
-	_riddleTopFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
-
 	_riddleBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 16, 1);
-	_riddleBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
-
 	_riddleBottomFrame = loadFrameFromPlanesInterleaved(&file, 16, 8);
+
+	// Apply mask to CLUT8 frames before palette conversion
+	Graphics::ManagedSurface *riddleFrames[] = {_riddleTopFrame, _riddleBackgroundFrame, _riddleBottomFrame};
+	for (int f = 0; f < 3; f++) {
+		Graphics::ManagedSurface *frame = riddleFrames[f];
+		for (int y = 0; y < frame->h; y++) {
+			for (int x = 0; x < frame->w; x++) {
+				int col = x / 16;
+				int bit = 15 - (x % 16);
+				if (!(riddleMask[col] & (1 << bit)))
+					frame->setPixel(x, y, 0);
+			}
+		}
+	}
+
+	_riddleTopFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+	_riddleBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
 	_riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
 
 	// Castle gate (game over background frame)
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 09d12e5b5a0..079966ef7fb 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -1466,19 +1466,34 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
 		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_riddleNailFrame, nailX, nailY, Common::Rect(0, 0, _riddleNailFrame->w, _riddleNailFrame->h), 0);
 	}
 
-	// Draw riddle frame borders (if available)
+	// Draw riddle frame borders (if available), clipped to viewport
 	if (_riddleTopFrame) {
-		surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, x, y, Common::Rect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h));
+		Common::Rect srcRect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h);
+		Common::Rect destRect(x, y, x + _riddleTopFrame->w, y + _riddleTopFrame->h);
+		destRect.clip(_viewArea);
+		srcRect = Common::Rect(destRect.left - x, destRect.top - y, destRect.right - x, destRect.bottom - y);
+		if (srcRect.isValidRect() && !srcRect.isEmpty())
+			surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, destRect.left, destRect.top, srcRect);
 		y += _riddleTopFrame->h;
 	}
 	if (_riddleBackgroundFrame) {
 		for (; y < maxWidth;) {
-			surface->copyRectToSurface((const Graphics::Surface)*_riddleBackgroundFrame, x, y, Common::Rect(0, 0, _riddleBackgroundFrame->w, _riddleBackgroundFrame->h));
+			Common::Rect srcRect(0, 0, _riddleBackgroundFrame->w, _riddleBackgroundFrame->h);
+			Common::Rect destRect(x, y, x + _riddleBackgroundFrame->w, y + _riddleBackgroundFrame->h);
+			destRect.clip(_viewArea);
+			srcRect = Common::Rect(destRect.left - x, destRect.top - y, destRect.right - x, destRect.bottom - y);
+			if (srcRect.isValidRect() && !srcRect.isEmpty())
+				surface->copyRectToSurface((const Graphics::Surface)*_riddleBackgroundFrame, destRect.left, destRect.top, srcRect);
 			y += _riddleBackgroundFrame->h;
 		}
 	}
 	if (_riddleBottomFrame) {
-		surface->copyRectToSurface((const Graphics::Surface)*_riddleBottomFrame, x, maxWidth, Common::Rect(0, 0, _riddleBottomFrame->w, _riddleBottomFrame->h - 1));
+		Common::Rect srcRect(0, 0, _riddleBottomFrame->w, _riddleBottomFrame->h - 1);
+		Common::Rect destRect(x, maxWidth, x + _riddleBottomFrame->w, maxWidth + _riddleBottomFrame->h - 1);
+		destRect.clip(_viewArea);
+		srcRect = Common::Rect(destRect.left - x, destRect.top - maxWidth, destRect.right - x, destRect.bottom - maxWidth);
+		if (srcRect.isValidRect() && !srcRect.isEmpty())
+			surface->copyRectToSurface((const Graphics::Surface)*_riddleBottomFrame, destRect.left, destRect.top, srcRect);
 	}
 
 	Common::Array<RiddleText> riddleMessages = _riddleList[riddle]._lines;
@@ -1510,7 +1525,7 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
 void CastleEngine::drawRiddleStringInSurface(const Common::String &str, int x, int y, uint32 fontColor, uint32 backColor, Graphics::Surface *surface) {
 	Common::String ustr = str;
 	ustr.toUppercase();
-	if (isDOS()) {
+	if (isDOS() || isAmiga() || isAtariST()) {
 		_fontRiddle.setBackground(backColor);
 		_fontRiddle.drawString(surface, ustr, x, y, _screenW, fontColor);
 	} else {


Commit: cc2cc2bb24fd6ac65199c55284cfe956f12bf90e
    https://github.com/scummvm/scummvm/commit/cc2cc2bb24fd6ac65199c55284cfe956f12bf90e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00

Commit Message:
FREESCAPE: load health indicator in castle amiga

Changed paths:
    engines/freescape/games/castle/amiga.cpp
    engines/freescape/games/castle/castle.cpp


diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 6dc21dd004d..658e80eb860 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -207,6 +207,19 @@ void CastleEngine::loadAssetsAmigaDemo() {
 	_spiritsMeterIndicatorFrame = loadFrameFromPlanesInterleaved(&file, 1, 10);
 	_spiritsMeterIndicatorFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
 
+	// Strength weight sprites (file 0x395F2, 1 word x 14 rows x 4 frames)
+	file.seek(0x395f2);
+	for (int i = 0; i < 4; i++) {
+		Graphics::ManagedSurface *frame = loadFrameFromPlanesInterleaved(&file, 1, 14);
+		frame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+		_strenghtWeightsFrames.push_back(frame);
+	}
+
+	// Strength background with bar (file 0x397B2, 5 words x 20 rows)
+	//file.seek(0x397b2);
+	//_strenghtBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 5, 4);
+	//_strenghtBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
 	// Key sprites (memory 0x3C096, 12 frames, 16x7 each, interleaved 4-plane)
 	file.seek(0x3c0b2);
 	for (int i = 0; i < 12; i++) {
@@ -393,6 +406,9 @@ void CastleEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 			Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));
 	}
 
+	// Draw energy meter (strength) - background placed at (0, 154) to match border
+	drawEnergyMeter(surface, Common::Point(40, 158));
+
 	// Draw spirit meter
 	if (_spiritsMeterIndicatorBackgroundFrame)
 		surface->copyRectToSurface((const Graphics::Surface)*_spiritsMeterIndicatorBackgroundFrame, 128, 160,
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 079966ef7fb..3c758f5a290 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -1535,17 +1535,13 @@ void CastleEngine::drawRiddleStringInSurface(const Common::String &str, int x, i
 }
 
 void CastleEngine::drawEnergyMeter(Graphics::Surface *surface, Common::Point origin) {
-	if (!_strenghtBackgroundFrame)
-		return;
-
-	surface->copyRectToSurface((const Graphics::Surface)*_strenghtBackgroundFrame, origin.x, origin.y, Common::Rect(0, 0, _strenghtBackgroundFrame->w, _strenghtBackgroundFrame->h));
-	if (!_strenghtBarFrame)
-		return;
+	if (_strenghtBackgroundFrame)
+		surface->copyRectToSurface((const Graphics::Surface)*_strenghtBackgroundFrame, origin.x, origin.y, Common::Rect(0, 0, _strenghtBackgroundFrame->w, _strenghtBackgroundFrame->h));
 
 	uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
 	uint32 back = 0;
 
-	if (isDOS())
+	if (isDOS() || isAmiga() || isAtariST())
 		back = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
 
 	int strength = _gameStateVars[k8bitVariableShield];
@@ -1555,14 +1551,16 @@ void CastleEngine::drawEnergyMeter(Graphics::Surface *surface, Common::Point ori
 
 	Common::Point barFrameOrigin = origin;
 
-	if (isDOS())
-		barFrameOrigin += Common::Point(5, 6 + extraYOffset);
-	else if (isSpectrum())
-		barFrameOrigin += Common::Point(0, 6 + extraYOffset);
-	else if (isCPC())
-		barFrameOrigin += Common::Point(0, 6 + extraYOffset);
+	if (_strenghtBarFrame) {
+		if (isDOS())
+			barFrameOrigin += Common::Point(5, 6 + extraYOffset);
+		else if (isSpectrum())
+			barFrameOrigin += Common::Point(0, 6 + extraYOffset);
+		else if (isCPC())
+			barFrameOrigin += Common::Point(0, 6 + extraYOffset);
 
-	surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtBarFrame, barFrameOrigin.x, barFrameOrigin.y, Common::Rect(0, 0, _strenghtBarFrame->w, _strenghtBarFrame->h), black);
+		surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtBarFrame, barFrameOrigin.x, barFrameOrigin.y, Common::Rect(0, 0, _strenghtBarFrame->w, _strenghtBarFrame->h), black);
+	}
 
 	Common::Point weightPoint;
 	int frameIdx = -1;
@@ -1586,6 +1584,10 @@ void CastleEngine::drawEnergyMeter(Graphics::Surface *surface, Common::Point ori
 		weightStep = 3;
 		weightOffset = 5;
 		rightWeightPos = 63;
+	} else if (isAmiga() || isAtariST()) {
+		weightStep = 3;
+		weightOffset = 10;
+		rightWeightPos = 62;
 	} else { // DOS
 		weightStep = 3;
 		weightOffset = 10;


Commit: fd609715b8ff68400b918cb8bf6e6b6ba9004d91
    https://github.com/scummvm/scummvm/commit/fd609715b8ff68400b918cb8bf6e6b6ba9004d91
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00

Commit Message:
FREESCAPE: loaded more frames in castle amiga, including cursors

Changed paths:
    engines/freescape/games/castle/amiga.cpp
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/castle.h


diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 658e80eb860..79932cf223a 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -21,6 +21,7 @@
 
 #include "common/file.h"
 #include "common/memstream.h"
+#include "graphics/cursorman.h"
 
 #include "audio/mods/protracker.h"
 
@@ -136,6 +137,7 @@ void CastleEngine::loadAssetsAmigaDemo() {
 	loadMessagesVariableSize(&file, 0x8bb2, 178);
 	loadRiddles(&file, 0x96c8 - 2 - 19 * 2, 19);
 
+
 	file.seek(0x11eec);
 	Common::Array<Graphics::ManagedSurface *> chars;
 	Common::Array<Graphics::ManagedSurface *> charsRiddle;
@@ -220,7 +222,9 @@ void CastleEngine::loadAssetsAmigaDemo() {
 	//_strenghtBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 5, 4);
 	//_strenghtBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
 
-	// Key sprites (memory 0x3C096, 12 frames, 16x7 each, interleaved 4-plane)
+	// Eye icon sprites (memory 0x3C096, 12 frames, 16x7 each, interleaved 4-plane)
+	// Used for strength/compass display at screen (224, 164). Header at 0x3C08E.
+	// TODO: load as separate eye icon member, not _keysBorderFrames
 	file.seek(0x3c0b2);
 	for (int i = 0; i < 12; i++) {
 		Graphics::ManagedSurface *frame = loadFrameFromPlanesInterleaved(&file, 1, 7);
@@ -228,6 +232,79 @@ void CastleEngine::loadAssetsAmigaDemo() {
 		_keysBorderFrames.push_back(frame);
 	}
 
+	// Crawl/Walk/Run + Sound indicators (memory 0x3838A, file 0x383A6, 5 frames, 48x12)
+	// Header (6 bytes) + mask (3 words = 6 bytes) + graphics.
+	// From assembly: frames 0-2 = crawl/walk/run at (96,118), frames 3-4 = sound off/on at (96,103)
+	file.seek(0x383a6 + 6 + 6); // skip header + mask
+	{
+		_menuCrawlIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+		_menuCrawlIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+		_menuWalkIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+		_menuWalkIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+		_menuRunIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+		_menuRunIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+		_menuFxOffIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+		_menuFxOffIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+		_menuFxOnIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+		_menuFxOnIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+	}
+
+	// Mouse pointer from paired sprites at mem $22E/$276 (file 0x24A/0x292).
+	// SPR0 at $22E + SPR1 at $276 form the diagonal arrow cursor.
+	// Each: 2 control words + 16 data rows (p0,p1 interleaved) + end marker = 72 bytes.
+	// SPR0 contributes color bits 0-1, SPR1 contributes bits 2-3 (4-bit combined).
+	{
+		_cursorW = 16;
+		_cursorH = 16;
+		_cursorData = new byte[16 * 16];
+		memset(_cursorData, 0, 16 * 16);
+		// Read SPR0 (bits 0-1)
+		file.seek(0x24A + 4); // skip control
+		for (int row = 0; row < 16; row++) {
+			uint16 p0 = file.readUint16BE();
+			uint16 p1 = file.readUint16BE();
+			for (int bit = 0; bit < 16; bit++) {
+				byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+				_cursorData[row * 16 + bit] = c;
+			}
+		}
+		// Overlay SPR1 (bits 2-3)
+		file.seek(0x292 + 4); // skip control
+		for (int row = 0; row < 16; row++) {
+			uint16 p0 = file.readUint16BE();
+			uint16 p1 = file.readUint16BE();
+			for (int bit = 0; bit < 16; bit++) {
+				byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+				_cursorData[row * 16 + bit] |= (c << 2);
+			}
+		}
+	}
+
+	// Crosshair pointer from paired sprites at mem $19E/$1E6 (file 0x1BA/0x202).
+	// Used outside the view area. Same format as diagonal arrow.
+	{
+		_crosshairData = new byte[16 * 16];
+		memset(_crosshairData, 0, 16 * 16);
+		file.seek(0x1BA + 4);
+		for (int row = 0; row < 16; row++) {
+			uint16 p0 = file.readUint16BE();
+			uint16 p1 = file.readUint16BE();
+			for (int bit = 0; bit < 16; bit++) {
+				byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+				_crosshairData[row * 16 + bit] = c;
+			}
+		}
+		file.seek(0x202 + 4);
+		for (int row = 0; row < 16; row++) {
+			uint16 p0 = file.readUint16BE();
+			uint16 p1 = file.readUint16BE();
+			for (int bit = 0; bit < 16; bit++) {
+				byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+				_crosshairData[row * 16 + bit] |= (c << 2);
+			}
+		}
+	}
+
 	// Flag animation (memory 0x3C340, 5 frames, 32x11 each, interleaved 4-plane)
 	file.seek(0x3c35c);
 	for (int i = 0; i < 5; i++) {
@@ -391,13 +468,7 @@ void CastleEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 	drawStringInSurface(_currentArea->_name, 97, 182, 0, 0, surface);
 	uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
 
-	// Draw last collected key at (224, 164)
-	if (!_keysCollected.empty() && !_keysBorderFrames.empty()) {
-		int k = int(_keysCollected.size()) - 1;
-		if (k < int(_keysBorderFrames.size()))
-			surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_keysBorderFrames[k], 224, 164,
-				Common::Rect(0, 0, _keysBorderFrames[k]->w, _keysBorderFrames[k]->h), black);
-	}
+	// TODO: Draw collected keys - key sprites location in binary still unknown
 
 	// Draw flag animation at (288, 5)
 	if (!_flagFrames.empty()) {
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 3c758f5a290..0c6d2495015 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -90,6 +90,10 @@ CastleEngine::CastleEngine(OSystem *syst, const ADGameDescription *gd) : Freesca
 	_keysBorderCLUT8 = nullptr;
 	_menu = nullptr;
 	_menuButtons = nullptr;
+	_cursorData = nullptr;
+	_crosshairData = nullptr;
+	_cursorW = 0;
+	_cursorH = 0;
 
 	_riddleTopFrame = nullptr;
 	_riddleBottomFrame = nullptr;
@@ -779,6 +783,35 @@ void CastleEngine::pressedKey(const int keycode) {
 		activate();
 }
 
+void CastleEngine::setAmigaCursor(bool crosshair) {
+	if (!_cursorData || !_crosshairData)
+		return;
+
+	static const byte cursorPalette[16 * 3] = {
+		0x00, 0x00, 0x00,  0x44, 0x44, 0x44,  0x66, 0x66, 0x66,  0x88, 0x88, 0x88,
+		0xAA, 0xAA, 0xAA,  0xCC, 0xCC, 0xCC,  0xAA, 0xAA, 0xAA,  0xCC, 0xCC, 0xCC,
+		0x44, 0x44, 0x44,  0x66, 0x66, 0x66,  0x88, 0x88, 0x88,  0xCC, 0xCC, 0xCC,
+		0x66, 0x66, 0x66,  0xCC, 0xCC, 0xCC,  0xAA, 0xAA, 0xAA,  0xEE, 0xEE, 0xEE,
+	};
+
+	byte *srcData = crosshair ? _crosshairData : _cursorData;
+	int scale = MAX(1, g_system->getWidth() / _screenW);
+	int sw = _cursorW * scale;
+	int sh = _cursorH * scale;
+	int hotX = crosshair ? 7 * scale : 1 * scale;
+	int hotY = crosshair ? 7 * scale : 1 * scale;
+
+	byte *scaledCursor = new byte[sw * sh];
+	for (int y = 0; y < sh; y++)
+		for (int x = 0; x < sw; x++)
+			scaledCursor[y * sw + x] = srcData[(y / scale) * _cursorW + (x / scale)];
+
+	Graphics::PixelFormat cursorFormat = Graphics::PixelFormat::createFormatCLUT8();
+	CursorMan.replaceCursor(scaledCursor, sw, sh, hotX, hotY, 0, false, &cursorFormat);
+	CursorMan.replaceCursorPalette(cursorPalette, 0, 16);
+	delete[] scaledCursor;
+}
+
 void CastleEngine::drawInfoMenu() {
 	PauseToken pauseToken = pauseEngine();
 	if (_savedScreen) {
@@ -832,31 +865,81 @@ void CastleEngine::drawInfoMenu() {
 			}
 		}
 	} else if (isAmiga() || isAtariST()) {
+		if (_cursorData)
+			setAmigaCursor(false); // arrow for the menu
+		else
+			CursorMan.setDefaultArrowCursor();
+		CursorMan.showMouse(true);
 		if (_menu)
 			surface->copyRectToSurface(*_menu, 47, 35, Common::Rect(0, 0, MIN<int>(_menu->w, surface->w - 47), MIN<int>(_menu->h, surface->h - 35)));
 
 		_gfx->readFromPalette(15, r, g, b);
 		front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
-		drawStringInSurface(Common::String::format("%07d", score), 166, 71, front, black, surface);
-		drawStringInSurface(centerAndPadString(Common::String::format("%s", _messagesList[135 + shield / 6].c_str()), 10), 151, 102, front, black, surface);
 
-		Common::String keysCollected = _messagesList[141];
-		Common::replace(keysCollected, "X", Common::String::format("%d", _keysCollected.size()));
-		drawStringInSurface(keysCollected, 103, 41, front, black, surface);
+		// Positions and formulas from assembly at FUN_1AE0:
+		// Score at (167, 71): move.w #$a7,d0; move.w #$47,d1
+		drawStringInSurface(Common::String::format("%07d", score), 167, 71, front, black, surface);
+
+		// Shield at (154, 102): move.w #$9a,d0; move.w #$66,d1
+		// Index = (shield - 1) / 4 (from: subq #1,d0; lsr #2,d0; muls #$c,d0)
+		// Amiga shield text at message indices 171-177 (skipping 174 which is empty)
+		{
+			static const int kAmigaShieldMsgIdx[] = {171, 172, 173, 175, 176, 177};
+			int shieldIdx = (shield > 0) ? (shield - 1) / 4 : 0;
+			if (shieldIdx > 5) shieldIdx = 5;
+			drawStringInSurface(centerAndPadString(_messagesList[kAmigaShieldMsgIdx[shieldIdx]], 10), 154, 102, front, black, surface);
+		}
 
-		Common::String spiritsDestroyedString = _messagesList[133];
-		Common::replace(spiritsDestroyedString, "X", Common::String::format("%d", spiritsDestroyed));
-		drawStringInSurface(spiritsDestroyedString, 145, 132, front, black, surface);
+		// Keys collected at (104, 41): move.w #$68,d0; move.w #$29,d1 (from FUN_22CC)
+		// Messages: 162="NO KEYS COLLECTED", 163="XX KEYS COLLECTED", 164=" 1 KEY COLLECTED"
+		{
+			Common::String keysText;
+			int numKeys = _keysCollected.size();
+			if (numKeys == 0)
+				keysText = _messagesList[162];
+			else if (numKeys == 1)
+				keysText = _messagesList[164];
+			else {
+				keysText = _messagesList[163];
+				Common::replace(keysText, "XX", Common::String::format("%2d", numKeys));
+			}
+			drawStringInSurface(keysText, 104, 41, front, black, surface);
+		}
 
-		for (int i = 0; i < int(_keysCollected.size()); i++) {
-			int y = 58 + (i / 2) * 18;
-			if (i % 2 == 0) {
-				surface->copyRectToSurfaceWithKey(*_keysBorderFrames[i], 58, y, Common::Rect(0, 0, _keysBorderFrames[i]->w, _keysBorderFrames[i]->h), black);
-				keyRects.push_back(Common::Rect(58, y, 58 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
-			} else {
-				surface->copyRectToSurfaceWithKey(*_keysBorderFrames[i], 80, y, Common::Rect(0, 0, _keysBorderFrames[i]->w, _keysBorderFrames[i]->h), black);
-				keyRects.push_back(Common::Rect(80, y, 80 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
+		// Spirits destroyed at (145, 133): move.w #$91,d0; move.w #$85,d1
+		// Messages: 156="NONE DESTROYED", 157=" XX DESTROYED "
+		{
+			Common::String spiritsText;
+			if (spiritsDestroyed == 0)
+				spiritsText = _messagesList[156];
+			else {
+				spiritsText = _messagesList[157];
+				Common::replace(spiritsText, "XX", Common::String::format("%2d", spiritsDestroyed));
 			}
+			drawStringInSurface(spiritsText, 145, 133, front, black, surface);
+		}
+
+		// Movement indicator at (96, 118) from assembly at 0x1BE8-0x1BF0
+		{
+			Graphics::ManagedSurface *moveIndicator = nullptr;
+			if (_playerStepIndex == 0)
+				moveIndicator = _menuCrawlIndicator;
+			else if (_playerStepIndex == 1)
+				moveIndicator = _menuWalkIndicator;
+			else
+				moveIndicator = _menuRunIndicator;
+			if (moveIndicator)
+				surface->copyRectToSurfaceWithKey((const Graphics::Surface)*moveIndicator, 96, 118,
+					Common::Rect(0, 0, moveIndicator->w, moveIndicator->h), black);
+		}
+
+		// Sound indicator at (96, 103) from assembly at 0x1C1A-0x1C22
+		// Frame 3 = sound off, frame 4 = sound on (offset $360, optionally +$120)
+		{
+			Graphics::ManagedSurface *sndIndicator = _menuFxOnIndicator ? _menuFxOnIndicator : _menuFxOffIndicator;
+			if (sndIndicator)
+				surface->copyRectToSurfaceWithKey((const Graphics::Surface)*sndIndicator, 96, 103,
+					Common::Rect(0, 0, sndIndicator->w, sndIndicator->h), black);
 		}
 	} else if (isSpectrum() || isCPC()) {
 		Common::Array<Common::String> lines;
@@ -942,6 +1025,12 @@ void CastleEngine::drawInfoMenu() {
 			case Common::EVENT_KEYDOWN:
 					cont = false;
 				break;
+			case Common::EVENT_MOUSEMOVE:
+				if ((isAmiga() || isAtariST()) && _cursorData && _crosshairData) {
+					Common::Point mp = getNormalizedPosition(event.mouse);
+					setAmigaCursor(_viewArea.contains(mp));
+				}
+				break;
 			case Common::EVENT_SCREEN_CHANGED:
 				_gfx->computeScreenViewport();
 				// TODO: properly refresh screen
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index ba04558213b..67c1106b456 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -48,6 +48,11 @@ public:
 	Graphics::ManagedSurface *_menuFxOnIndicator;
 	Graphics::ManagedSurface *_menuFxOffIndicator;
 	Graphics::ManagedSurface *_menu;
+	byte *_cursorData;       // diagonal arrow (outside view area)
+	byte *_crosshairData;    // crosshair (# pointer, inside view area)
+	int _cursorW;
+	int _cursorH;
+	void setAmigaCursor(bool crosshair);
 
 	void beforeStarting() override;
 	void initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target) override;


Commit: c6e76087452d01a7e19a07263bcedc79da7362bf
    https://github.com/scummvm/scummvm/commit/c6e76087452d01a7e19a07263bcedc79da7362bf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00

Commit Message:
FREESCAPE: added pulsating surface detection and rendering for dark and castle (amiga/atari)

Changed paths:
    engines/freescape/area.cpp
    engines/freescape/area.h
    engines/freescape/freescape.cpp
    engines/freescape/games/castle/amiga.cpp
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/dark/amiga.cpp
    engines/freescape/games/dark/atari.cpp
    engines/freescape/gfx.cpp
    engines/freescape/gfx.h
    engines/freescape/loaders/8bitBinaryLoader.cpp


diff --git a/engines/freescape/area.cpp b/engines/freescape/area.cpp
index ac288f6c159..e25e31cffc0 100644
--- a/engines/freescape/area.cpp
+++ b/engines/freescape/area.cpp
@@ -74,6 +74,7 @@ Area::Area(uint16 areaID_, uint16 areaFlags_, ObjectMap *objectsByID_, ObjectMap
 	_underFireBackgroundColor = 255;
 	_inkColor = 255;
 	_paperColor = 255;
+	_colorCycling = false;
 
 	_gasPocketRadius = 0;
 
diff --git a/engines/freescape/area.h b/engines/freescape/area.h
index 62a0106acfd..30d5406fb55 100644
--- a/engines/freescape/area.h
+++ b/engines/freescape/area.h
@@ -98,6 +98,7 @@ public:
 	uint8 _inkColor;
 	uint8 _paperColor;
 	uint8 _extraColor[4];
+	bool _colorCycling; // Amiga/Atari: bit 14 of area header enables COLOR15 cycling
 	ColorReMap _colorRemaps;
 
 	uint32 _lastTick;
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index ef0e4654650..e9716409aa1 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -473,6 +473,7 @@ void FreescapeEngine::drawBackground() {
 }
 
 void FreescapeEngine::drawFrame() {
+	_gfx->updateColorCycling();
 	int farClipPlane = _farClipPlane;
 	if (_currentArea->isOutside())
 		farClipPlane *= 100;
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 79932cf223a..1d51207ba19 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -175,6 +175,15 @@ void CastleEngine::loadAssetsAmigaDemo() {
 
 	loadPalettes(&file, 0x151a6);
 
+	// COLOR15 cycling table (mem $8B78, file 0x8B94): 14 entries of 12-bit Amiga RGB + 0xFFFF end.
+	// From assembly: interrupt handler at $12BA cycles $DFF19E through this table every 4 frames.
+	file.seek(0x8b94);
+	while (true) {
+		uint16 val = file.readUint16BE();
+		if (val == 0xFFFF) break;
+		_gfx->_colorCyclingTable.push_back(val);
+	}
+
 	file.seek(0x2be96); // Area 255
 	_areaMap[255] = load8bitArea(&file, 16);
 
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 0c6d2495015..05dcae8d4e5 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -612,6 +612,10 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
 
 	swapPalette(areaID);
 
+	// Enable/disable COLOR15 cycling based on per-area flag (Amiga/Atari)
+	if ((isAmiga() || isAtariST()) && _currentArea)
+		_gfx->_colorCyclingTimer = _currentArea->_colorCycling ? 0 : -1;
+
 	if (isCPC())
 		updateCPCSpritesPalette();
 
diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 738bd3e430a..70c2c53b13f 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -64,6 +64,23 @@ void DarkEngine::loadAssetsAmigaFullGame() {
 	}
 	titleSurface->convertToInPlace(_gfx->_texturePixelFormat, pal.data(), pal.size());
 	_title = titleSurface;
+
+	// Dark Side: COLOR5 palette cycling from assembly interrupt handler at $10E4.
+	// Cycles $DFF18A (COLOR5) every 2 frames through 30 entries.
+	{
+		static const uint16 kDarkSideCyclingTable[] = {
+			0x000, 0xE6D, 0x600, 0x900, 0xC00, 0xF00, 0xF30, 0xF60,
+			0xF90, 0xFC0, 0xFF0, 0xAF0, 0x5F0, 0x6F8, 0x7FD, 0x7EF,
+			0xBDF, 0xDDF, 0xBCF, 0x9BF, 0x7BF, 0x6BF, 0x5AF, 0x4AF,
+			0x29F, 0x18F, 0x07F, 0x04C, 0x02A, 0x007
+		};
+		for (int i = 0; i < 30; i++)
+			_gfx->_colorCyclingTable.push_back(kDarkSideCyclingTable[i]);
+	}
+	_gfx->_colorCyclingPaletteIndex = 5;
+	_gfx->_colorCyclingSpeed = 1;
+	_gfx->_colorCyclingTimer = 0; // always active
+
 	file.close();
 
 	Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.drk", "0.drk", 798);
@@ -99,26 +116,6 @@ void DarkEngine::loadAssetsAmigaFullGame() {
 
 	_fontLoaded = true;
 
-	GeometricObject *obj = nullptr;
-	obj = (GeometricObject *)_areaMap[15]->objectWithID(18);
-	assert(obj);
-	obj->_cyclingColors = true;
-
-	obj = (GeometricObject *)_areaMap[15]->objectWithID(26);
-	assert(obj);
-	obj->_cyclingColors = true;
-
-	for (int i = 0; i < 3; i++) {
-		int16 id = 227 + i * 6 - 2;
-		for (int j = 0; j < 2; j++) {
-			//debugC(1, kFreescapeDebugParser, "Restoring object %d to from ECD %d", id, index);
-			obj = (GeometricObject *)_areaMap[255]->objectWithID(id);
-			assert(obj);
-			obj->_cyclingColors = true;
-			id--;
-		}
-	}
-
 	for (auto &area : _areaMap) {
 		// Center and pad each area name so we do not have to do it at each frame
 		area._value->_name = centerAndPadString(area._value->_name, 26);
diff --git a/engines/freescape/games/dark/atari.cpp b/engines/freescape/games/dark/atari.cpp
index f7f52cb525d..8b25e6c266e 100644
--- a/engines/freescape/games/dark/atari.cpp
+++ b/engines/freescape/games/dark/atari.cpp
@@ -30,6 +30,22 @@ void DarkEngine::loadAssetsAtariFullGame() {
 	Common::File file;
 	file.open("0.drk");
 	_title = loadAndConvertNeoImage(&file, 0x13ec);
+
+	// Atari ST Dark Side: same COLOR5 cycling as Amiga.
+	{
+		static const uint16 kDarkSideCyclingTable[] = {
+			0x000, 0xE6D, 0x600, 0x900, 0xC00, 0xF00, 0xF30, 0xF60,
+			0xF90, 0xFC0, 0xFF0, 0xAF0, 0x5F0, 0x6F8, 0x7FD, 0x7EF,
+			0xBDF, 0xDDF, 0xBCF, 0x9BF, 0x7BF, 0x6BF, 0x5AF, 0x4AF,
+			0x29F, 0x18F, 0x07F, 0x04C, 0x02A, 0x007
+		};
+		for (int i = 0; i < 30; i++)
+			_gfx->_colorCyclingTable.push_back(kDarkSideCyclingTable[i]);
+	}
+	_gfx->_colorCyclingPaletteIndex = 5;
+	_gfx->_colorCyclingSpeed = 1;
+	_gfx->_colorCyclingTimer = 0;
+
 	file.close();
 
 	Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.drk", "0.drk", 840);
@@ -55,26 +71,6 @@ void DarkEngine::loadAssetsAtariFullGame() {
 	loadGlobalObjects(stream, 0x32f6, 24);
 	loadSoundsFx(stream, 0x266e8, 11);
 
-	GeometricObject *obj = nullptr;
-	obj = (GeometricObject *)_areaMap[15]->objectWithID(18);
-	assert(obj);
-	obj->_cyclingColors = true;
-
-	obj = (GeometricObject *)_areaMap[15]->objectWithID(26);
-	assert(obj);
-	obj->_cyclingColors = true;
-
-	for (int i = 0; i < 3; i++) {
-		int16 id = 227 + i * 6 - 2;
-		for (int j = 0; j < 2; j++) {
-			//debugC(1, kFreescapeDebugParser, "Restoring object %d to from ECD %d", id, index);
-			obj = (GeometricObject *)_areaMap[255]->objectWithID(id);
-			assert(obj);
-			obj->_cyclingColors = true;
-			id--;
-		}
-	}
-
 	for (auto &area : _areaMap) {
 		// Center and pad each area name so we do not have to do it at each frame
 		area._value->_name = centerAndPadString(area._value->_name, 26);
diff --git a/engines/freescape/gfx.cpp b/engines/freescape/gfx.cpp
index 49a4a6fe1c7..0c1df92916c 100644
--- a/engines/freescape/gfx.cpp
+++ b/engines/freescape/gfx.cpp
@@ -48,6 +48,10 @@ Renderer::Renderer(int screenW, int screenH, Common::RenderMode renderMode, bool
 	_palette = nullptr;
 	_colorMap = nullptr;
 	_colorRemaps = nullptr;
+	_colorCyclingIndex = 0;
+	_colorCyclingTimer = -1;
+	_colorCyclingPaletteIndex = 15;
+	_colorCyclingSpeed = 3;
 	_renderMode = renderMode;
 	_isAccelerated = false;
 	_debugRenderBoundingBoxes = false;
@@ -299,11 +303,32 @@ void Renderer::setColorMap(ColorMap *colorMap_) {
 }
 
 void Renderer::readFromPalette(uint8 index, uint8 &r, uint8 &g, uint8 &b) {
+	// Amiga/Atari: COLOR15 hardware palette cycling.
+	// From assembly: interrupt handler at $12BA cycles $DFF19E (COLOR15)
+	// through table at $8B78 every 4 frames, gated by per-area flag at $7EC2.
+	// Only palette index 15 is affected; other indices render normally.
+	if (index == _colorCyclingPaletteIndex && _colorCyclingTimer >= 0 && !_colorCyclingTable.empty()) {
+		uint16 val = _colorCyclingTable[_colorCyclingIndex];
+		r = ((val >> 8) & 0xF) * 17;
+		g = ((val >> 4) & 0xF) * 17;
+		b = (val & 0xF) * 17;
+		return;
+	}
 	r = _palette[3 * index + 0];
 	g = _palette[3 * index + 1];
 	b = _palette[3 * index + 2];
 }
 
+void Renderer::updateColorCycling() {
+	if (_colorCyclingTimer < 0 || _colorCyclingTable.empty())
+		return;
+	_colorCyclingTimer--;
+	if (_colorCyclingTimer < 0) {
+		_colorCyclingTimer = _colorCyclingSpeed;
+		_colorCyclingIndex = (_colorCyclingIndex + 1) % _colorCyclingTable.size();
+	}
+}
+
 uint8 Renderer::indexFromColor(uint8 r, uint8 g, uint8 b) {
 	for (int i = 0; i < 16; i++) {
 		if (r == _palette[3 * i + 0] && g == _palette[3 * i + 1] && b == _palette[3 * i + 2])
@@ -533,12 +558,27 @@ bool Renderer::getRGBAt(uint8 index, uint8 ecolor, uint8 &r1, uint8 &g1, uint8 &
 	}
 
 	if (_renderMode == Common::kRenderAmiga || _renderMode == Common::kRenderAtariST) {
+		// Hardware palette cycling: if the main color index matches the cycling
+		// palette entry and cycling is active, use the cycling color directly.
+		// This must happen BEFORE color pair resolution since on real hardware
+		// the Copper list sets the color register globally.
+		if (index == _colorCyclingPaletteIndex && _colorCyclingTimer >= 0 && !_colorCyclingTable.empty()) {
+			readFromPalette(index, r1, g1, b1);
+			r2 = r1; g2 = g1; b2 = b1;
+			if (ecolor > 0)
+				readFromPalette(ecolor, r2, g2, b2);
+			return true;
+		}
+
 		if (_colorPair[index] > 0) {
 			int color = 0;
 			color = _colorPair[index] & 0xf;
 			readFromPalette(color, r1, g1, b1);
 			color = _colorPair[index] >> 4;
 			readFromPalette(color, r2, g2, b2);
+			// Also apply cycling to ecolor if it matches the cycling index
+			if (ecolor > 0 && ecolor == _colorCyclingPaletteIndex && _colorCyclingTimer >= 0 && !_colorCyclingTable.empty())
+				readFromPalette(ecolor, r2, g2, b2);
 			return true;
 		} else if (_colorRemaps && _colorRemaps->contains(index)) {
 			int color = (*_colorRemaps)[index];
diff --git a/engines/freescape/gfx.h b/engines/freescape/gfx.h
index ba9187c2a3a..1986ea5db35 100644
--- a/engines/freescape/gfx.h
+++ b/engines/freescape/gfx.h
@@ -290,6 +290,16 @@ public:
 	Common::Point _shakeOffset;
 	byte _stipples[16][128];
 
+	// Amiga/Atari hardware palette cycling for pulsating surfaces.
+	// Castle Master: COLOR15, every 4 frames, per-area gated.
+	// Dark Side: COLOR5, every 2 frames, always active.
+	Common::Array<uint16> _colorCyclingTable;
+	int _colorCyclingIndex;
+	int _colorCyclingTimer;     // -1 = disabled, >=0 = active
+	int _colorCyclingPaletteIndex; // which palette entry to cycle (5 or 15)
+	int _colorCyclingSpeed;     // frames between changes (2 or 4)
+	void updateColorCycling();
+
 	int _scale;
 
 	// debug flags
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index ccd8cbb2215..b033012f6d3 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -621,7 +621,17 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
 	Common::String name;
 	uint32 base = file->pos();
 	debugC(1, kFreescapeDebugParser, "Area base: %x", base);
-	uint8 areaFlags = readField(file, 8);
+	// On Amiga/Atari, the first field is 16-bit. Bit 14 (0x4000) enables COLOR15 cycling.
+	// From assembly at $10076: move.w (a1),d0; bclr #$e,d0 → extracts bit 14.
+	bool areaColorCycling = false;
+	uint8 areaFlags;
+	if (isAmiga() || isAtariST()) {
+		uint16 fullWord = file->readUint16BE();
+		areaColorCycling = (fullWord & 0x4000) != 0;
+		areaFlags = fullWord & 0xFF;
+	} else {
+		areaFlags = readField(file, 8);
+	}
 	uint8 numberOfObjects = readField(file, 8);
 	uint8 areaNumber = readField(file, 8);
 
@@ -800,6 +810,7 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
 	area->_usualBackgroundColor = usualBackgroundColor;
 	area->_underFireBackgroundColor = underFireBackgroundColor;
 
+	area->_colorCycling = areaColorCycling;
 	area->_extraColor[0] = extraColor[0];
 	area->_extraColor[1] = extraColor[1];
 	area->_extraColor[2] = extraColor[2];


Commit: e4f5bc30f914feb38e90cc90de0fe2b7f5443b2f
    https://github.com/scummvm/scummvm/commit/e4f5bc30f914feb38e90cc90de0fe2b7f5443b2f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00

Commit Message:
FREESCAPE: added an option for WASD movement and shift for run

Changed paths:
    engines/freescape/detection.cpp
    engines/freescape/detection.h
    engines/freescape/events.cpp
    engines/freescape/freescape.cpp
    engines/freescape/freescape.h
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/castle.h
    engines/freescape/games/eclipse/eclipse.cpp
    engines/freescape/metaengine.cpp
    engines/freescape/movement.cpp


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 68a09784e0b..f5ef949d445 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -570,7 +570,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmstradCPC,
 		ADGF_DEMO,
-		GUIO2(GUIO_NOMIDI, GUIO_RENDERCPC)
+		GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"totaleclipse",
@@ -584,7 +584,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		ADGF_DEMO | GF_ZX_DEMO_MICROHOBBY,
-		GUIO2(GUIO_NOMIDI, GUIO_RENDERZX)
+		GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"totaleclipse",
@@ -598,7 +598,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		ADGF_DEMO | GF_ZX_DEMO_CRASH,
-		GUIO2(GUIO_NOMIDI, GUIO_RENDERZX)
+		GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"totaleclipse",
@@ -607,7 +607,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		ADGF_NO_FLAGS,
-		GUIO2(GUIO_NOMIDI, GUIO_RENDERZX)
+		GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"totaleclipse2",
@@ -616,7 +616,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		ADGF_NO_FLAGS,
-		GUIO2(GUIO_NOMIDI, GUIO_RENDERZX)
+		GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"totaleclipse",
@@ -701,7 +701,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformDOS,
 		ADGF_NO_FLAGS,
-		GUIO4(GUIO_NOMIDI, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_MODERN_MOVEMENT)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		// Erbe Software release
@@ -718,7 +718,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformDOS,
 		ADGF_NO_FLAGS,
-		GUIO4(GUIO_NOMIDI, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_MODERN_MOVEMENT)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"totaleclipse", // Tape relese
@@ -802,7 +802,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		GF_ZX_RETAIL,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX, GAMEOPTION_WASD_CONTROLS)
 	},
 	// Disc release
 	{
@@ -812,7 +812,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		GF_ZX_DISC,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX, GAMEOPTION_WASD_CONTROLS)
 	},
 	// Spanish release was disc-only?
 	{
@@ -822,7 +822,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::ES_ESP,
 		Common::kPlatformZX,
 		GF_ZX_DISC,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"castlemaster",
@@ -836,7 +836,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformDOS,
 		ADGF_DEMO,
-		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+		GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"castlemaster",
@@ -849,7 +849,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmiga,
 		ADGF_UNSTABLE | ADGF_DEMO,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	// Stampede Amiga, Issue 1, July 1990
 	{
@@ -863,7 +863,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmiga,
 		ADGF_UNSTABLE | ADGF_DEMO,
-		GUIO3(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA)
+		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"castlemaster",
@@ -877,7 +877,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmstradCPC,
 		ADGF_UNSTABLE,
-		GUIO2(GUIO_NOMIDI, GUIO_RENDERCPC)
+		GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_TRAVEL_ROCK, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"castlemaster",
@@ -893,7 +893,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::UNK_LANG,
 		Common::kPlatformDOS,
 		ADGF_PIRATED,
-		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+		GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"castlemaster",
@@ -909,7 +909,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::UNK_LANG, // Multi-language
 		Common::kPlatformDOS,
 		ADGF_NO_FLAGS,
-		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+		GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"castlemaster",
@@ -925,7 +925,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::UNK_LANG, // Multi-language
 		Common::kPlatformDOS,
 		ADGF_NO_FLAGS,
-		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+		GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"castlemaster",
@@ -941,7 +941,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::UNK_LANG, // Multi-language
 		Common::kPlatformDOS,
 		ADGF_UNSUPPORTED, // Compressed with lzexe
-		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+		GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"castlemaster",
@@ -957,7 +957,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::UNK_LANG, // Multi-language
 		Common::kPlatformDOS,
 		ADGF_UNSUPPORTED, // Game data offset are different
-		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+		GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	{
 		"castlemaster",
@@ -973,7 +973,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::ES_ESP,
 		Common::kPlatformDOS,
 		ADGF_UNSTABLE,
-		GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+		GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
 	},
 	// 3D Construction Kit games
 	{
diff --git a/engines/freescape/detection.h b/engines/freescape/detection.h
index 0bbc27bba04..5e0a562c11f 100644
--- a/engines/freescape/detection.h
+++ b/engines/freescape/detection.h
@@ -35,9 +35,10 @@
 // Driller options
 #define GAMEOPTION_AUTOMATIC_DRILLING   GUIO_GAMEOPTIONS8
 
-// Castle Master options
+// Castle Master / Total Eclipse options
 
 #define GAMEOPTION_TRAVEL_ROCK   GUIO_GAMEOPTIONS9
+#define GAMEOPTION_WASD_CONTROLS GUIO_GAMEOPTIONS11
 
 
 #endif
diff --git a/engines/freescape/events.cpp b/engines/freescape/events.cpp
index db5f0039d51..d47713feef5 100644
--- a/engines/freescape/events.cpp
+++ b/engines/freescape/events.cpp
@@ -42,14 +42,23 @@ bool EventManagerWrapper::pollEvent(Common::Event &event) {
 				break;
 			_currentActionDown = event.customType;
 			_keyRepeatTime = time + kKeyRepeatInitialDelay;
+			// Track all simultaneously held actions
+			if (Common::find(_activeActions.begin(), _activeActions.end(), event.customType) == _activeActions.end())
+				_activeActions.push_back(event.customType);
 			break;
 		case Common::EVENT_CUSTOM_ENGINE_ACTION_END:
 			if (event.customType == kActionEscape)
 				break;
 			if (event.customType == _currentActionDown) {
-				// Only stop firing events if it's the current key
 				_currentActionDown = kActionNone;
 			}
+			// Remove from active actions
+			for (uint i = 0; i < _activeActions.size(); i++) {
+				if (_activeActions[i] == event.customType) {
+					_activeActions.remove_at(i);
+					break;
+				}
+			}
 			break;
 		case Common::EVENT_KEYDOWN:
 			if (event.kbd == Common::KEYCODE_ESCAPE || event.kbd == Common::KEYCODE_F5)
@@ -102,6 +111,7 @@ void EventManagerWrapper::purgeKeyboardEvents() {
 	_delegate->purgeKeyboardEvents();
 	_currentKeyDown.reset();
 	_currentActionDown = kActionNone;
+	_activeActions.clear();
 	_keyRepeatTime = 0;
 }
 
@@ -120,7 +130,7 @@ void EventManagerWrapper::clearExitEvents() {
 }
 
 bool EventManagerWrapper::isActionActive(const Common::CustomEventType &action) {
-	return _currentActionDown == action;
+	return Common::find(_activeActions.begin(), _activeActions.end(), action) != _activeActions.end();
 }
 
 bool EventManagerWrapper::isKeyPressed() {
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index e9716409aa1..d8ee03c26bc 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -131,6 +131,10 @@ FreescapeEngine::FreescapeEngine(OSystem *syst, const ADGameDescription *gd)
 	if (!Common::parseBool(ConfMan.get("smooth_movement"), _smoothMovement))
 		error("Failed to parse bool from smooth_movement option");
 
+	_useWASDControls = false;
+	if (ConfMan.hasKey("wasd_controls"))
+		Common::parseBool(ConfMan.get("wasd_controls"), _useWASDControls);
+
 	if (isDriller() || isSpaceStationOblivion() || isDark())
 		_smoothMovement = false;
 
@@ -181,6 +185,7 @@ FreescapeEngine::FreescapeEngine(OSystem *syst, const ADGameDescription *gd)
 
 	// TODO: this is not the same for every game
 	_playerStepIndex = 6;
+	_savedPlayerStepIndex = -1;
 	_playerSteps.push_back(1);
 	_playerSteps.push_back(2);
 	_playerSteps.push_back(5);
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 12c3788a2ed..6f191661109 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -104,6 +104,7 @@ enum FreescapeAction {
 	kActionRunMode,
 	kActionWalkMode,
 	kActionCrawlMode,
+	kActionRunModifier, // Shift-to-run: held = run, released = walk
 	kActionSelectPrince,
 	kActionSelectPrincess,
 	kActionQuit,
@@ -176,7 +177,8 @@ private:
 	Common::EventManager *_delegate;
 
 	Common::KeyState _currentKeyDown;
-	Common::CustomEventType _currentActionDown;
+	Common::CustomEventType _currentActionDown; // last action for key repeat
+	Common::Array<Common::CustomEventType> _activeActions; // all currently held actions
 	uint32 _keyRepeatTime;
 };
 
@@ -351,6 +353,7 @@ public:
 	bool _invertY;
 
 	bool _smoothMovement;
+	bool _useWASDControls;
 	// Player movement state
 	bool _moveForward;
 	bool _moveBackward;
@@ -429,6 +432,7 @@ public:
 	uint16 _stepUpDistance;
 
 	int _playerStepIndex;
+	int _savedPlayerStepIndex; // saved before shift-to-run, restored on release
 	Common::Array<int> _playerSteps;
 
 	Common::Point crossairPosToMousePos(const Common::Point &crossairPos);
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 05dcae8d4e5..c28eea204bc 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -521,7 +521,7 @@ void CastleEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *inf
 	act = new Common::Action("WALK", _("Walk"));
 	act->setCustomEngineActionEvent(kActionWalkMode);
 	act->addDefaultInputMapping("JOY_B");
-	act->addDefaultInputMapping("w");
+	act->addDefaultInputMapping(_useWASDControls ? "2" : "w");
 	engineKeyMap->addAction(act);
 
 	act = new Common::Action("CRAWL", _("Crawl"));
@@ -537,8 +537,17 @@ void CastleEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *inf
 
 	act = new Common::Action("ACTIVATE", _("Activate"));
 	act->setCustomEngineActionEvent(kActionActivate);
-	act->addDefaultInputMapping("a");
+	act->addDefaultInputMapping(_useWASDControls ? "e" : "a");
 	engineKeyMap->addAction(act);
+
+	if (_useWASDControls) {
+		act = new Common::Action("RUNMOD", _("Run (hold)"));
+		act->setCustomEngineActionEvent(kActionRunModifier);
+		act->addDefaultInputMapping("LSHIFT");
+		act->addDefaultInputMapping("RSHIFT");
+		act->addDefaultInputMapping("JOY_LEFT_TRIGGER");
+		engineKeyMap->addAction(act);
+	}
 }
 
 void CastleEngine::beforeStarting() {
@@ -780,6 +789,20 @@ void CastleEngine::pressedKey(const int keycode) {
 		}
 		_playerStepIndex = 0;
 		insertTemporaryMessage(_messagesList[13], _countdown - 2);
+	} else if (keycode == kActionRunModifier) {
+		// Shift-to-run: save current mode, switch to run while held
+		if (_playerStepIndex == 2)
+			return; // already running
+		if (_playerHeightNumber == 0) {
+			if (_gameStateVars[k8bitVariableShield] <= 3)
+				return;
+			if (!rise()) {
+				return;
+			}
+			_gameStateVars[k8bitVariableCrawling] = 0;
+		}
+		_savedPlayerStepIndex = _playerStepIndex;
+		_playerStepIndex = 2;
 	} else if (keycode == kActionFaceForward) {
 		_pitch = 0;
 		updateCamera();
@@ -787,6 +810,16 @@ void CastleEngine::pressedKey(const int keycode) {
 		activate();
 }
 
+void CastleEngine::releasedKey(const int keycode) {
+	if (keycode == kActionRunModifier) {
+		// Shift released: restore the mode from before running
+		if (_savedPlayerStepIndex >= 0) {
+			_playerStepIndex = _savedPlayerStepIndex;
+			_savedPlayerStepIndex = -1;
+		}
+	}
+}
+
 void CastleEngine::setAmigaCursor(bool crosshair) {
 	if (!_cursorData || !_crosshairData)
 		return;
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 67c1106b456..207c0ec9b5f 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -82,6 +82,7 @@ public:
 	void drawLiftingGate(Graphics::Surface *surface);
 	void drawDroppingGate(Graphics::Surface *surface);
 	void pressedKey(const int keycode) override;
+	void releasedKey(const int keycode) override;
 	void checkSensors() override;
 	void updateTimeVariables() override;
 	void drawBackground() override;
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index b3bce7135f3..bf093963bbd 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -263,21 +263,30 @@ void EclipseEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *in
 
 	act = new Common::Action("ROTR", _("Rotate right"));
 	act->setCustomEngineActionEvent(kActionRotateRight);
-	act->addDefaultInputMapping("w");
+	act->addDefaultInputMapping(_useWASDControls ? "e" : "w");
 	engineKeyMap->addAction(act);
 
 	// I18N: Illustrates the angle at which you turn left or right.
 	act = new Common::Action("CHNGANGLE", _("Change angle"));
 	act->setCustomEngineActionEvent(kActionIncreaseAngle);
-	act->addDefaultInputMapping("a");
+	act->addDefaultInputMapping(_useWASDControls ? "v" : "a");
 	engineKeyMap->addAction(act);
 
 	// I18N: STEP SIZE: Measures the size of one movement in the direction you are facing (1-250 standard distance units (SDUs))
 	act = new Common::Action("CHNGSTEPSIZE", _("Change step size"));
 	act->setCustomEngineActionEvent(kActionChangeStepSize);
-	act->addDefaultInputMapping("s");
+	act->addDefaultInputMapping(_useWASDControls ? "x" : "s");
 	engineKeyMap->addAction(act);
 
+	if (_useWASDControls) {
+		act = new Common::Action("RUNMOD", _("Sprint (hold)"));
+		act->setCustomEngineActionEvent(kActionRunModifier);
+		act->addDefaultInputMapping("LSHIFT");
+		act->addDefaultInputMapping("RSHIFT");
+		act->addDefaultInputMapping("JOY_LEFT_TRIGGER");
+		engineKeyMap->addAction(act);
+	}
+
 	act = new Common::Action("TGGLHEIGHT", _("Toggle height"));
 	act->setCustomEngineActionEvent(kActionToggleRiseLower);
 	act->addDefaultInputMapping("JOY_B");
@@ -540,6 +549,12 @@ void EclipseEngine::pressedKey(const int keycode) {
 		updateCamera();
 	} else if (keycode == kActionToggleFlashlight) {
 		_flashlightOn = !_flashlightOn;
+	} else if (keycode == kActionRunModifier) {
+		// Shift-to-sprint: save current step, switch to max while held
+		if (_savedPlayerStepIndex < 0) {
+			_savedPlayerStepIndex = _playerStepIndex;
+			_playerStepIndex = (int)_playerSteps.size() - 1;
+		}
 	}
 }
 
@@ -619,6 +634,13 @@ bool EclipseEngine::onScreenControls(Common::Point mouse) {
 void EclipseEngine::releasedKey(const int keycode) {
 	if (keycode == kActionRiseOrFlyUp)
 		_resting = false;
+	else if (keycode == kActionRunModifier) {
+		// Shift released: restore previous step size
+		if (_savedPlayerStepIndex >= 0) {
+			_playerStepIndex = _savedPlayerStepIndex;
+			_savedPlayerStepIndex = -1;
+		}
+	}
 }
 
 void EclipseEngine::drawAnalogClock(Graphics::Surface *surface, int x, int y, uint32 colorHand1, uint32 colorHand2, uint32 colorBack) {
diff --git a/engines/freescape/metaengine.cpp b/engines/freescape/metaengine.cpp
index 198c3f87927..4b9bb8685d5 100644
--- a/engines/freescape/metaengine.cpp
+++ b/engines/freescape/metaengine.cpp
@@ -146,6 +146,18 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 			0
 		}
 	},
+	{
+		GAMEOPTION_WASD_CONTROLS,
+		{
+			// I18N: Use modern FPS-style controls: WASD for movement, Shift to run
+			_s("WASD controls"),
+			_s("Use WASD keys for movement and Shift to run"),
+			"wasd_controls",
+			false,
+			0,
+			0
+		}
+	},
 	AD_EXTRA_GUI_OPTIONS_TERMINATOR
 };
 
diff --git a/engines/freescape/movement.cpp b/engines/freescape/movement.cpp
index 56b47946853..f61677c5e6c 100644
--- a/engines/freescape/movement.cpp
+++ b/engines/freescape/movement.cpp
@@ -37,6 +37,8 @@ void FreescapeEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *
 	act->addDefaultInputMapping("UP");
 	act->addDefaultInputMapping("JOY_UP");
 	act->addDefaultInputMapping("o");
+	if (_useWASDControls)
+		act->addDefaultInputMapping("w");
 	engineKeyMap->addAction(act);
 
 	act = new Common::Action(Common::kStandardActionMoveDown, _("Down"));
@@ -44,20 +46,24 @@ void FreescapeEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *
 	act->addDefaultInputMapping("DOWN");
 	act->addDefaultInputMapping("JOY_DOWN");
 	act->addDefaultInputMapping("k");
+	if (_useWASDControls)
+		act->addDefaultInputMapping("s");
 	engineKeyMap->addAction(act);
 
 	act = new Common::Action(Common::kStandardActionMoveLeft, _("Strafe left"));
 	act->setCustomEngineActionEvent(kActionMoveLeft);
 	act->addDefaultInputMapping("LEFT");
 	act->addDefaultInputMapping("JOY_LEFT");
-	// act->addDefaultInputMapping("q");
+	if (_useWASDControls)
+		act->addDefaultInputMapping("a");
 	engineKeyMap->addAction(act);
 
 	act = new Common::Action(Common::kStandardActionMoveRight, _("Strafe right"));
 	act->setCustomEngineActionEvent(kActionMoveRight);
 	act->addDefaultInputMapping("RIGHT");
 	act->addDefaultInputMapping("JOY_RIGHT");
-	// act->addDefaultInputMapping("w");
+	if (_useWASDControls)
+		act->addDefaultInputMapping("d");
 	engineKeyMap->addAction(act);
 
 	act = new Common::Action("SHOOT", _("Shoot"));
@@ -448,13 +454,10 @@ void FreescapeEngine::updatePlayerMovementClassic(float deltaTime) {
 void FreescapeEngine::updatePlayerMovementSmooth(float deltaTime) {
 	if (_moveForward && !_eventManager->isActionActive(kActionMoveUp))
 		_moveForward = false;
-
 	if (_moveBackward && !_eventManager->isActionActive(kActionMoveDown))
 		_moveBackward = false;
-
 	if (_strafeLeft && !_eventManager->isActionActive(kActionMoveLeft))
 		_strafeLeft = false;
-
 	if (_strafeRight && !_eventManager->isActionActive(kActionMoveRight))
 		_strafeRight = false;
 




More information about the Scummvm-git-logs mailing list