[Scummvm-git-logs] scummvm master -> 14ae29f3ba5999d8e4606d335bb07f90282d56b8

neuromancer noreply at scummvm.org
Fri Mar 27 15:04:05 UTC 2026


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

Summary:
8a2ecac6c6 FREESCAPE: screen animation in driller amiga
1d604d61af FREESCAPE: parse and display jetpack indicator in dark amiga
1443dc9945 FREESCAPE: parse and display other indicators in dark amiga
b6b11bfb2e FREESCAPE: allow compass to spin in dark amiga
14ae29f3ba FREESCAPE: eclipse heartbeat fixes


Commit: 8a2ecac6c67687a7c81e7ff0c7858ee441d90ea0
    https://github.com/scummvm/scummvm/commit/8a2ecac6c67687a7c81e7ff0c7858ee441d90ea0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-27T08:41:45+01:00

Commit Message:
FREESCAPE: screen animation in driller amiga

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


diff --git a/engines/freescape/games/driller/amiga.cpp b/engines/freescape/games/driller/amiga.cpp
index 4b2615f87d8..3721eb991ea 100644
--- a/engines/freescape/games/driller/amiga.cpp
+++ b/engines/freescape/games/driller/amiga.cpp
@@ -19,6 +19,7 @@
  *
  */
 #include "common/file.h"
+#include "common/random.h"
 
 #include "freescape/freescape.h"
 #include "freescape/games/driller/driller.h"
@@ -132,6 +133,36 @@ void DrillerEngine::loadIndicatorSprites(Common::SeekableReadStream *file, byte
 	}
 }
 
+void DrillerEngine::loadEarthquakeSprites(Common::SeekableReadStream *file, byte *palette, int earthquakeOffset) {
+	// Seismograph monitor: 2 word columns (32px) × 11 rows, decoded from overlapping
+	// frames in a continuous sprite buffer. SPREQW={1,10,$200} means 2 columns, 11 rows
+	// (dbra counts). SPREQM={$8000,$07FF} masks out pixel 0 and pixels 21-31, leaving
+	// a visible area of 20×11 pixels. The original picks random byte offsets in steps
+	// of 32 from two ranges:
+	//   Sound ON:  2048..2528 (step 32) → 16 frames of dense seismic activity
+	//   Sound OFF: 0..480 (step 32) → 16 frames of sparse activity
+	// We precompute all 32 frames, storing only the 20×11 visible region.
+	uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
+	static const int offsets[] = {
+		0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 480,
+		2048, 2080, 2112, 2144, 2176, 2208, 2240, 2272, 2304, 2336, 2368, 2400, 2432, 2464, 2496, 2528
+	};
+
+	for (int i = 0; i < 32; i++) {
+		// Decode the full 32×11 sprite, then extract the 20-pixel visible region
+		Graphics::ManagedSurface full;
+		full.create(32, 11, _gfx->_texturePixelFormat);
+		full.fillRect(Common::Rect(0, 0, 32, 11), black);
+		decodeAmigaSprite(file, &full, earthquakeOffset + offsets[i], 2, 11, palette, _gfx->_texturePixelFormat);
+
+		auto *surf = new Graphics::ManagedSurface();
+		surf->create(20, 11, _gfx->_texturePixelFormat);
+		surf->fillRect(Common::Rect(0, 0, 20, 11), black);
+		surf->copyRectToSurface(full, 0, 0, Common::Rect(1, 0, 21, 11));
+		_earthquakeSprites.push_back(surf);
+	}
+}
+
 void DrillerEngine::loadCompassStrips(Common::SeekableReadStream *file, byte *palette,
 		int pitchStripOffset, int yawCogOffset) {
 	uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
@@ -219,6 +250,7 @@ void DrillerEngine::loadAssetsAmigaFullGame() {
 		loadRigSprites(&file, 0x2407A);
 		loadIndicatorSprites(&file, palette, 0x26F9A, 0x27222, 0x24D88, 0x26912);
 		loadCompassStrips(&file, palette, 0x23316, 0x26F4C);
+		loadEarthquakeSprites(&file, palette, 0x27560);
 		free(palette);
 	} else if (_variant & GF_AMIGA_BUDGET) {
 		file.open("lift.neo");
@@ -259,6 +291,7 @@ void DrillerEngine::loadAssetsAmigaFullGame() {
 		if (palette) {
 			loadIndicatorSprites(&file, palette, 0x1E288, 0x1E510, 0x1C5D6, 0x1DC00);
 			loadCompassStrips(&file, palette, 0x1AB64, 0x1E23A);
+			loadEarthquakeSprites(&file, palette, 0x1E84E);
 		}
 		free(palette);
 
@@ -518,6 +551,16 @@ void DrillerEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 			Common::Rect(_compassYawFrames[rot]->w, _compassYawFrames[rot]->h), transparent);
 	}
 
+	// Seismograph monitor (SPREQL): animated noise at x=50, y=1 (20×11 visible pixels).
+	// The original updates every 4th tick picking a random frame.
+	// Sound-on frames (indices 16-31) show dense activity; sound-off (0-15) sparse.
+	if (!_earthquakeSprites.empty()) {
+		if ((_ticks & 3) == 0)
+			_earthquakeLastFrame = 16 + _rnd->getRandomNumber(15);
+		surface->copyRectToSurface(*_earthquakeSprites[_earthquakeLastFrame], 50, 1,
+			Common::Rect(0, 0, 20, 11));
+	}
+
 	// Quit indicator (ABORTSQ): shows on the console when quit is initiated.
 	// First click: shutter rolls down (frames 0-6), then shows 3 empty squares (frame 7).
 	// Clicks 2-4: squares fill in (frames 8-10). Fourth click = quit confirmed.
@@ -590,6 +633,7 @@ void DrillerEngine::initAmigaAtari() {
 	_compassPitchStrip = nullptr;
 	_quitConfirmCounter = 0;
 	_quitStartTicks = 0;
+	_earthquakeLastFrame = 16;
 	_quitArea = Common::Rect(188, 5, 208, 13);
 	_borderExtraTexture = nullptr;
 
diff --git a/engines/freescape/games/driller/driller.h b/engines/freescape/games/driller/driller.h
index fcbd20e71cc..2a8d874ba58 100644
--- a/engines/freescape/games/driller/driller.h
+++ b/engines/freescape/games/driller/driller.h
@@ -115,9 +115,12 @@ private:
 	int _quitConfirmCounter;  // 0=not quitting, 1-4=waiting for confirmations
 	int _quitStartTicks;      // _ticks when quit was initiated (for shutter animation)
 	Common::Rect _quitArea;   // click area for quit button on Amiga/Atari console
+	Common::Array<Graphics::ManagedSurface *> _earthquakeSprites; // seismograph monitor frames
+	int _earthquakeLastFrame;
 	void loadRigSprites(Common::SeekableReadStream *file, int sprigsOffset);
 	void loadIndicatorSprites(Common::SeekableReadStream *file, byte *palette,
 		int stepOffset, int angleOffset, int vehicleOffset, int quitOffset);
+	void loadEarthquakeSprites(Common::SeekableReadStream *file, byte *palette, int earthquakeOffset);
 
 	// Compass indicators loaded from executable
 	Graphics::ManagedSurface *_compassPitchStrip;  // pitch: 32px wide × (144+29) rows scrolling strip


Commit: 1d604d61af39bef99747fd0937446de4cbaf126c
    https://github.com/scummvm/scummvm/commit/1d604d61af39bef99747fd0937446de4cbaf126c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-27T13:26:18+01:00

Commit Message:
FREESCAPE: parse and display jetpack indicator in dark amiga

Changed paths:
    engines/freescape/freescape.cpp
    engines/freescape/freescape.h
    engines/freescape/games/dark/amiga.cpp
    engines/freescape/games/dark/dark.cpp
    engines/freescape/games/dark/dark.h
    engines/freescape/games/driller/amiga.cpp


diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 49e9dafeea0..02902bd8f0f 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -1251,6 +1251,32 @@ void FreescapeEngine::clearTemporalMessages() {
 	_temporaryMessageDeadlines.clear();
 }
 
+void FreescapeEngine::decodeAmigaSprite(Common::SeekableReadStream *file, Graphics::ManagedSurface *surf,
+		int dataOffset, int widthWords, int height, byte *palette) {
+	for (int y = 0; y < height; y++) {
+		for (int col = 0; col < widthWords; col++) {
+			int off = dataOffset + (y * widthWords + col) * 8;
+			file->seek(off);
+			uint16 p0 = file->readUint16BE();
+			uint16 p1 = file->readUint16BE();
+			uint16 p2 = file->readUint16BE();
+			uint16 p3 = file->readUint16BE();
+			for (int bit = 0; bit < 16; bit++) {
+				byte colorIdx = 0;
+				if (p0 & (0x8000 >> bit)) colorIdx |= 1;
+				if (p1 & (0x8000 >> bit)) colorIdx |= 2;
+				if (p2 & (0x8000 >> bit)) colorIdx |= 4;
+				if (p3 & (0x8000 >> bit)) colorIdx |= 8;
+				if (colorIdx == 0)
+					continue;
+				uint32 color = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
+					palette[colorIdx * 3], palette[colorIdx * 3 + 1], palette[colorIdx * 3 + 2]);
+				surf->setPixel(col * 16 + bit, y, color);
+			}
+		}
+	}
+}
+
 byte *FreescapeEngine::getPaletteFromNeoImage(Common::SeekableReadStream *stream, int offset) {
 	stream->seek(offset);
 	Image::NeoDecoder decoder;
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 202dceaa245..499b61615be 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -281,6 +281,8 @@ public:
 	Graphics::Surface *loadBundledImage(const Common::String &name, bool appendRenderMode = true);
 	byte *getPaletteFromNeoImage(Common::SeekableReadStream *stream, int offset);
 	Graphics::ManagedSurface *loadAndConvertNeoImage(Common::SeekableReadStream *stream, int offset, byte *palette = nullptr);
+	void decodeAmigaSprite(Common::SeekableReadStream *file, Graphics::ManagedSurface *surf,
+		int dataOffset, int widthWords, int height, byte *palette);
 	Graphics::ManagedSurface *loadAndConvertScrImage(Common::SeekableReadStream *stream);
 	Graphics::ManagedSurface *loadFrame(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, uint32 front);
 	Graphics::ManagedSurface *loadFrameCPCIndexed(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int widthBytes, int height);
diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 70c2c53b13f..4a24433a313 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -19,6 +19,7 @@
  *
  */
 #include "common/file.h"
+#include "common/system.h"
 
 #include "graphics/palette.h"
 
@@ -116,12 +117,135 @@ void DarkEngine::loadAssetsAmigaFullGame() {
 
 	_fontLoaded = true;
 
+	loadJetpackRawFrames(stream);
+
 	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);
 	}
 }
 
+void DarkEngine::loadJetpackRawFrames(Common::SeekableReadStream *file) {
+	// The executable stream still includes the 0x1C-byte GEMDOS header, so the
+	// original program addresses need to be converted back to file offsets here.
+	static const int kGemdosHeaderSize = 0x1C;
+	// Original Amiga layout:
+	// - transition strip at prog 0x23B9E, 9 frames, stride 0x160
+	// - crouch frame at prog 0x2481E
+	static const int kTransitionBaseOffset = 0x23B9E + kGemdosHeaderSize;
+	static const int kTransitionFrameCount = 9;
+	static const int kCrouchFrameOffset = 0x2481E + kGemdosHeaderSize;
+	static const int kFrameSize = 0x160; // 2 word columns * 22 rows * 8 bytes/row
+	_jetpackTransitionFrames.clear();
+	for (int i = 0; i < kTransitionFrameCount; i++) {
+		file->seek(kTransitionBaseOffset + i * kFrameSize);
+		Common::Array<byte> raw(kFrameSize);
+		file->read(raw.data(), kFrameSize);
+		_jetpackTransitionFrames.push_back(raw);
+	}
+
+	file->seek(kCrouchFrameOffset);
+	_jetpackCrouchFrame.resize(kFrameSize);
+	file->read(_jetpackCrouchFrame.data(), kFrameSize);
+
+	_jetpackIndicatorStateInitialized = false;
+	_jetpackIndicatorTransitionDirection = 0;
+}
+
+void DarkEngine::drawJetpackIndicator(Graphics::Surface *surface) {
+	static const int kTransitionFrameCount = 9;
+	static const uint32 kFrameDelayMs = 60;
+	static const int kVisibleLeftX = 109;
+	static const int kSourceLeftPadding = 13;
+	static const int kDrawBaseX = kVisibleLeftX - kSourceLeftPadding;
+	static const int kHeight = 22;
+	static const int kWidthWords = 2;
+	static const int kDrawY = 175;
+	static const uint16 kMaskWords[kWidthWords] = { 0xFFF8, 0x00FF };
+	static const int kFlyingBaseFrame = 0;
+	static const int kGroundStandingFrame = 8;
+	static const uint16 kJetpackColors[16] = {
+		0x000, 0x222, 0x000, 0x000,
+		0x000, 0x000, 0x444, 0x666,
+		0x000, 0x800, 0xA00, 0xF00,
+		0xF80, 0xFD0, 0x000, 0x000
+	};
+
+	if (_jetpackTransitionFrames.size() != kTransitionFrameCount || _jetpackCrouchFrame.empty())
+		return;
+
+	if (!_jetpackIndicatorStateInitialized) {
+		_jetpackIndicatorStateInitialized = true;
+		_jetpackIndicatorLastFlyMode = _flyMode;
+		_jetpackIndicatorTransitionFrame = _flyMode ? 0 : kTransitionFrameCount - 1;
+		_jetpackIndicatorTransitionDirection = 0;
+		_jetpackIndicatorNextFrameMillis = 0;
+	} else if (_jetpackIndicatorLastFlyMode != _flyMode) {
+		// The original routines at 0x89F4 and 0x8ACC play 8->0 on enable
+		// and 0->8 on disable via $D18.
+		_jetpackIndicatorLastFlyMode = _flyMode;
+		_jetpackIndicatorTransitionFrame = _flyMode ? kTransitionFrameCount - 1 : 0;
+		_jetpackIndicatorTransitionDirection = _flyMode ? -1 : 1;
+		_jetpackIndicatorNextFrameMillis = g_system->getMillis() + kFrameDelayMs;
+	}
+
+	if (_jetpackIndicatorTransitionDirection != 0) {
+		uint32 now = g_system->getMillis();
+		while (now >= _jetpackIndicatorNextFrameMillis) {
+			int nextFrame = _jetpackIndicatorTransitionFrame + _jetpackIndicatorTransitionDirection;
+			if (nextFrame < 0 || nextFrame >= kTransitionFrameCount) {
+				_jetpackIndicatorTransitionDirection = 0;
+				break;
+			}
+			_jetpackIndicatorTransitionFrame = nextFrame;
+			_jetpackIndicatorNextFrameMillis += kFrameDelayMs;
+		}
+	}
+
+	const byte *raw = nullptr;
+	if (_jetpackIndicatorTransitionDirection != 0) {
+		raw = _jetpackTransitionFrames[_jetpackIndicatorTransitionFrame].data();
+	} else if (_flyMode) {
+		// 0x89F4 leaves the grounded strip on frame 0 after the 8->0 startup
+		// transition. The later 0x24988 path in FUN_106A is an incremental
+		// overlay, so the redraw-from-scratch UI still needs the base frame.
+		raw = _jetpackTransitionFrames[kFlyingBaseFrame].data();
+	} else {
+		// Dark uses two grounded stances. The engine-side stance maps to the
+		// same standing/crouch split used by the other Dark ports.
+		raw = (_playerHeightNumber == 0) ? _jetpackCrouchFrame.data() : _jetpackTransitionFrames[kGroundStandingFrame].data();
+	}
+
+	for (int y = 0; y < kHeight; y++) {
+		for (int col = 0; col < kWidthWords; col++) {
+			int off = (y * kWidthWords + col) * 8;
+			uint16 srcPlanes[4] = {
+				(uint16)((raw[off] << 8) | raw[off + 1]),
+				(uint16)((raw[off + 2] << 8) | raw[off + 3]),
+				(uint16)((raw[off + 4] << 8) | raw[off + 5]),
+				(uint16)((raw[off + 6] << 8) | raw[off + 7])
+			};
+			for (int bit = 0; bit < 16; bit++) {
+				if (kMaskWords[col] & (0x8000 >> bit))
+					continue;
+
+				int px = col * 16 + bit;
+				byte colorIdx = 0;
+				if (srcPlanes[0] & (0x8000 >> bit)) colorIdx |= 1;
+				if (srcPlanes[1] & (0x8000 >> bit)) colorIdx |= 2;
+				if (srcPlanes[2] & (0x8000 >> bit)) colorIdx |= 4;
+				if (srcPlanes[3] & (0x8000 >> bit)) colorIdx |= 8;
+				uint16 colorWord = kJetpackColors[colorIdx];
+				uint32 color = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
+					((colorWord >> 8) & 0xF) * 17,
+					((colorWord >> 4) & 0xF) * 17,
+					(colorWord & 0xF) * 17);
+				surface->setPixel(kDrawBaseX + px, kDrawY + y, color);
+			}
+		}
+	}
+}
+
 void DarkEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 	uint32 white = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0xFF, 0xFF, 0xFF);
 	uint32 yellow = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0xEE, 0xCC, 0x00);
@@ -157,6 +281,8 @@ void DarkEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 	drawString(kDarkFontSmall, _currentArea->_name, 32, 151, grey8, greyA, transparent, surface);
 	drawBinaryClock(surface, 6, 110, white, grey);
 
+	drawJetpackIndicator(surface);
+
 	int x = 229;
 	int y = 180;
 	for (int i = 0; i < _maxShield / 2; i++) {
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index 591254ea2a8..0bfcc5181cb 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -90,6 +90,11 @@ DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEn
 	_initialShield = 15;
 
 	_jetFuelSeconds = _initialEnergy * 6;
+	_jetpackIndicatorStateInitialized = false;
+	_jetpackIndicatorLastFlyMode = false;
+	_jetpackIndicatorTransitionFrame = 0;
+	_jetpackIndicatorTransitionDirection = 0;
+	_jetpackIndicatorNextFrameMillis = 0;
 }
 
 DarkEngine::~DarkEngine() {
@@ -302,6 +307,8 @@ void DarkEngine::initGameState() {
 
 	_angleRotationIndex = 0;
 	_playerStepIndex = 6;
+	_jetpackIndicatorStateInitialized = false;
+	_jetpackIndicatorTransitionDirection = 0;
 
 	// Start background music
 	if (isAmiga() && !_musicData.empty()) {
@@ -1059,6 +1066,8 @@ Common::Error DarkEngine::loadGameStreamExtended(Common::SeekableReadStream *str
 		uint16 key = stream->readUint16LE();
 		_exploredAreas[key] = stream->readUint32LE();
 	}
+	_jetpackIndicatorStateInitialized = false;
+	_jetpackIndicatorTransitionDirection = 0;
 	return Common::kNoError;
 }
 
diff --git a/engines/freescape/games/dark/dark.h b/engines/freescape/games/dark/dark.h
index ea2f1e733a9..c1a0d3c11ba 100644
--- a/engines/freescape/games/dark/dark.h
+++ b/engines/freescape/games/dark/dark.h
@@ -104,6 +104,21 @@ public:
 	Font _fontBig;
 	Font _fontMedium;
 	Font _fontSmall;
+
+	// Dark Side Amiga stores the grounded jetpack indicator states as raw
+	// 4-plane bitplane data. The executable drives those frames through a tiny
+	// fixed color ramp, so the renderer keeps the raw planes and applies a
+	// hardcoded palette at draw time.
+	Common::Array<Common::Array<byte>> _jetpackTransitionFrames;
+	Common::Array<byte> _jetpackCrouchFrame;
+	bool _jetpackIndicatorStateInitialized;
+	bool _jetpackIndicatorLastFlyMode;
+	int _jetpackIndicatorTransitionFrame;
+	int _jetpackIndicatorTransitionDirection;
+	uint32 _jetpackIndicatorNextFrameMillis;
+	void loadJetpackRawFrames(Common::SeekableReadStream *file);
+	void drawJetpackIndicator(Graphics::Surface *surface);
+
 	int _soundIndexRestoreECD;
 	int _soundIndexDestroyECD;
 	Audio::SoundHandle _soundFxHandleJetpack;
diff --git a/engines/freescape/games/driller/amiga.cpp b/engines/freescape/games/driller/amiga.cpp
index 3721eb991ea..5b883e593a0 100644
--- a/engines/freescape/games/driller/amiga.cpp
+++ b/engines/freescape/games/driller/amiga.cpp
@@ -27,33 +27,6 @@
 
 namespace Freescape {
 
-static void decodeAmigaSprite(Common::SeekableReadStream *file, Graphics::ManagedSurface *surf,
-		int dataOffset, int widthWords, int height, byte *palette,
-		const Graphics::PixelFormat &fmt) {
-	for (int y = 0; y < height; y++) {
-		for (int col = 0; col < widthWords; col++) {
-			int off = dataOffset + (y * widthWords + col) * 8;
-			file->seek(off);
-			uint16 p0 = file->readUint16BE();
-			uint16 p1 = file->readUint16BE();
-			uint16 p2 = file->readUint16BE();
-			uint16 p3 = file->readUint16BE();
-			for (int bit = 0; bit < 16; bit++) {
-				byte colorIdx = 0;
-				if (p0 & (0x8000 >> bit)) colorIdx |= 1;
-				if (p1 & (0x8000 >> bit)) colorIdx |= 2;
-				if (p2 & (0x8000 >> bit)) colorIdx |= 4;
-				if (p3 & (0x8000 >> bit)) colorIdx |= 8;
-				if (colorIdx == 0)
-					continue;
-				uint32 color = fmt.ARGBToColor(0xFF,
-					palette[colorIdx * 3], palette[colorIdx * 3 + 1], palette[colorIdx * 3 + 2]);
-				surf->setPixel(col * 16 + bit, y, color);
-			}
-		}
-	}
-}
-
 void DrillerEngine::loadRigSprites(Common::SeekableReadStream *file, int sprigsOffset) {
 	// SPRIGS: 2 word columns × 25 rows × 5 frames, stride=$1A0 (416 bytes)
 	const int frameStride = 0x1A0;
@@ -77,7 +50,7 @@ void DrillerEngine::loadRigSprites(Common::SeekableReadStream *file, int sprigsO
 		auto *surf = new Graphics::ManagedSurface();
 		surf->create(32, 25, _gfx->_texturePixelFormat);
 		surf->fillRect(Common::Rect(0, 0, 32, 25), transparent);
-		decodeAmigaSprite(file, surf, sprigsOffset + (f + 1) * frameStride, 2, 25, palette, _gfx->_texturePixelFormat);
+		decodeAmigaSprite(file, surf, sprigsOffset + (f + 1) * frameStride, 2, 25, palette);
 		_rigSprites.push_back(surf);
 	}
 
@@ -93,7 +66,7 @@ void DrillerEngine::loadIndicatorSprites(Common::SeekableReadStream *file, byte
 		auto *surf = new Graphics::ManagedSurface();
 		surf->create(16, 4, _gfx->_texturePixelFormat);
 		surf->fillRect(Common::Rect(0, 0, 16, 4), transparent);
-		decodeAmigaSprite(file, surf, stepOffset + f * 40, 1, 4, palette, _gfx->_texturePixelFormat);
+		decodeAmigaSprite(file, surf, stepOffset + f * 40, 1, 4, palette);
 		_stepSprites.push_back(surf);
 	}
 
@@ -102,7 +75,7 @@ void DrillerEngine::loadIndicatorSprites(Common::SeekableReadStream *file, byte
 		auto *surf = new Graphics::ManagedSurface();
 		surf->create(16, 4, _gfx->_texturePixelFormat);
 		surf->fillRect(Common::Rect(0, 0, 16, 4), transparent);
-		decodeAmigaSprite(file, surf, angleOffset + f * 40, 1, 4, palette, _gfx->_texturePixelFormat);
+		decodeAmigaSprite(file, surf, angleOffset + f * 40, 1, 4, palette);
 		_angleSprites.push_back(surf);
 	}
 
@@ -114,7 +87,7 @@ void DrillerEngine::loadIndicatorSprites(Common::SeekableReadStream *file, byte
 			auto *surf = new Graphics::ManagedSurface();
 			surf->create(64, 43, _gfx->_texturePixelFormat);
 			surf->fillRect(Common::Rect(0, 0, 64, 43), black);
-			decodeAmigaSprite(file, surf, vehicleOffset + f * 0x580, 4, 43, palette, _gfx->_texturePixelFormat);
+			decodeAmigaSprite(file, surf, vehicleOffset + f * 0x580, 4, 43, palette);
 			_vehicleSprites.push_back(surf);
 		}
 	}
@@ -127,7 +100,7 @@ void DrillerEngine::loadIndicatorSprites(Common::SeekableReadStream *file, byte
 			auto *surf = new Graphics::ManagedSurface();
 			surf->create(32, 8, _gfx->_texturePixelFormat);
 			surf->fillRect(Common::Rect(0, 0, 32, 8), black);
-			decodeAmigaSprite(file, surf, quitOffset + f * 0x90, 2, 8, palette, _gfx->_texturePixelFormat);
+			decodeAmigaSprite(file, surf, quitOffset + f * 0x90, 2, 8, palette);
 			_quitSprites.push_back(surf);
 		}
 	}
@@ -153,7 +126,7 @@ void DrillerEngine::loadEarthquakeSprites(Common::SeekableReadStream *file, byte
 		Graphics::ManagedSurface full;
 		full.create(32, 11, _gfx->_texturePixelFormat);
 		full.fillRect(Common::Rect(0, 0, 32, 11), black);
-		decodeAmigaSprite(file, &full, earthquakeOffset + offsets[i], 2, 11, palette, _gfx->_texturePixelFormat);
+		decodeAmigaSprite(file, &full, earthquakeOffset + offsets[i], 2, 11, palette);
 
 		auto *surf = new Graphics::ManagedSurface();
 		surf->create(20, 11, _gfx->_texturePixelFormat);
@@ -175,7 +148,7 @@ void DrillerEngine::loadCompassStrips(Common::SeekableReadStream *file, byte *pa
 		_compassPitchStrip = new Graphics::ManagedSurface();
 		_compassPitchStrip->create(32, totalRows, _gfx->_texturePixelFormat);
 		_compassPitchStrip->fillRect(Common::Rect(0, 0, 32, totalRows), black);
-		decodeAmigaSprite(file, _compassPitchStrip, pitchStripOffset, 2, totalRows, palette, _gfx->_texturePixelFormat);
+		decodeAmigaSprite(file, _compassPitchStrip, pitchStripOffset, 2, totalRows, palette);
 	}
 
 	// Yaw compass (SPRCOG): pre-render all 72 rotation frames.


Commit: 1443dc9945468b99a9adc82bb59140e9e5594c99
    https://github.com/scummvm/scummvm/commit/1443dc9945468b99a9adc82bb59140e9e5594c99
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-27T14:07:58+01:00

Commit Message:
FREESCAPE: parse and display other indicators in dark amiga

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


diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 4a24433a313..367aa650221 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -29,6 +29,47 @@
 
 namespace Freescape {
 
+namespace {
+
+static const int kAmigaGemdosHeaderSize = 0x1C;
+
+static int amigaProgToFile(int address) {
+	return address + kAmigaGemdosHeaderSize;
+}
+
+static void decodeMaskedAmigaSprite(Common::SeekableReadStream *file, Graphics::ManagedSurface *surf,
+		int dataOffset, int widthWords, int height, const uint16 *maskWords,
+		const Graphics::PixelFormat &pixelFormat, const byte *palette) {
+	for (int y = 0; y < height; y++) {
+		for (int col = 0; col < widthWords; col++) {
+			int off = dataOffset + (y * widthWords + col) * 8;
+			file->seek(off);
+			uint16 p0 = file->readUint16BE();
+			uint16 p1 = file->readUint16BE();
+			uint16 p2 = file->readUint16BE();
+			uint16 p3 = file->readUint16BE();
+			for (int bit = 0; bit < 16; bit++) {
+				if (maskWords[col] & (0x8000 >> bit))
+					continue;
+
+				byte colorIdx = 0;
+				if (p0 & (0x8000 >> bit)) colorIdx |= 1;
+				if (p1 & (0x8000 >> bit)) colorIdx |= 2;
+				if (p2 & (0x8000 >> bit)) colorIdx |= 4;
+				if (p3 & (0x8000 >> bit)) colorIdx |= 8;
+				if (colorIdx == 0)
+					continue;
+
+				uint32 color = pixelFormat.ARGBToColor(0xFF,
+					palette[colorIdx * 3], palette[colorIdx * 3 + 1], palette[colorIdx * 3 + 2]);
+				surf->setPixel(col * 16 + bit, y, color);
+			}
+		}
+	}
+}
+
+} // namespace
+
 void DarkEngine::loadAssetsAmigaFullGame() {
 	Common::File file;
 	file.open("0.drk");
@@ -117,7 +158,11 @@ void DarkEngine::loadAssetsAmigaFullGame() {
 
 	_fontLoaded = true;
 
+	byte *palette = getPaletteFromNeoImage(stream, 0x1b762);
+	loadAmigaCompass(stream, palette);
+	loadAmigaIndicatorSprites(stream, palette);
 	loadJetpackRawFrames(stream);
+	free(palette);
 
 	for (auto &area : _areaMap) {
 		// Center and pad each area name so we do not have to do it at each frame
@@ -125,16 +170,103 @@ void DarkEngine::loadAssetsAmigaFullGame() {
 	}
 }
 
+void DarkEngine::loadAmigaIndicatorSprites(Common::SeekableReadStream *file, byte *palette) {
+	if (!palette)
+		return;
+
+	uint32 transparent = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0, 0, 0);
+
+	_amigaCompassNeedleFrames.clear();
+	for (int frame = 0; frame < 13; frame++) {
+		auto *surf = new Graphics::ManagedSurface();
+		surf->create(32, 3, _gfx->_texturePixelFormat);
+		surf->fillRect(Common::Rect(0, 0, 32, 3), transparent);
+		decodeAmigaSprite(file, surf, amigaProgToFile(0x2784E) + frame * 0x30, 2, 3, palette);
+		_amigaCompassNeedleFrames.push_back(surf);
+	}
+
+	static const uint16 kLeftMasks[2] = { 0xE000, 0x01FF };
+	static const uint16 kRightMasks[2] = { 0xFF80, 0x003F };
+	static const int kSideFrameOrder[4] = { 3, 2, 1, 0 };
+
+	_amigaCompassLeftFrames.clear();
+	for (int phase = 0; phase < 4; phase++) {
+		int frameIndex = kSideFrameOrder[phase];
+		auto *surf = new Graphics::ManagedSurface();
+		surf->create(32, 21, _gfx->_texturePixelFormat);
+		surf->fillRect(Common::Rect(0, 0, 32, 21), transparent);
+		decodeMaskedAmigaSprite(file, surf, amigaProgToFile(0x29B34) + frameIndex * 0x150, 2, 21,
+			kLeftMasks, _gfx->_texturePixelFormat, palette);
+		_amigaCompassLeftFrames.push_back(surf);
+	}
+
+	_amigaCompassRightFrames.clear();
+	for (int phase = 0; phase < 4; phase++) {
+		int frameIndex = kSideFrameOrder[phase];
+		auto *surf = new Graphics::ManagedSurface();
+		surf->create(32, 21, _gfx->_texturePixelFormat);
+		surf->fillRect(Common::Rect(0, 0, 32, 21), transparent);
+		decodeMaskedAmigaSprite(file, surf, amigaProgToFile(0x2A07E) + frameIndex * 0x150, 2, 21,
+			kRightMasks, _gfx->_texturePixelFormat, palette);
+		_amigaCompassRightFrames.push_back(surf);
+	}
+}
+
+void DarkEngine::loadAmigaCompass(Common::SeekableReadStream *file, byte *palette) {
+	if (!palette)
+		return;
+
+	uint32 transparent = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0, 0, 0);
+	uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
+
+	Graphics::ManagedSurface base;
+	base.create(32, 5, _gfx->_texturePixelFormat);
+	base.fillRect(Common::Rect(0, 0, 32, 5), transparent);
+	decodeAmigaSprite(file, &base, amigaProgToFile(0x238B4), 2, 5, palette);
+
+	_amigaCompassYawFrames.clear();
+	file->seek(amigaProgToFile(0x234CC));
+	uint32 cursorMaskBase = file->readUint32BE();
+	for (int pos = 0; pos < 72; pos++) {
+		auto *surf = new Graphics::ManagedSurface();
+		surf->create(32, 5, _gfx->_texturePixelFormat);
+		surf->fillRect(Common::Rect(0, 0, 32, 5), transparent);
+		surf->copyRectToSurface(base, 0, 0, Common::Rect(base.w, base.h));
+
+		int rowOffset = amigaProgToFile(0x234D0) + ((pos >> 3) & 0xFFFE);
+		int shift = pos & 0xF;
+		for (int row = 0; row < 5; row++) {
+			file->seek(rowOffset + row * 14);
+			uint32 longVal = file->readUint32BE();
+			uint16 wordVal = file->readUint16BE();
+			uint32 mask = ((longVal << shift) & 0xFFFFFFFF);
+			mask |= (((uint32)wordVal << shift) >> 16) & 0xFFFF;
+			mask |= cursorMaskBase;
+
+			for (int bit = 0; bit < 32; bit++) {
+				if ((mask & (0x80000000 >> bit)) == 0 && base.getPixel(bit, row) != transparent)
+					surf->setPixel(bit, row, black);
+			}
+		}
+
+		_amigaCompassYawFrames.push_back(surf);
+	}
+
+	_amigaCompassPitchMarker = new Graphics::ManagedSurface();
+	_amigaCompassPitchMarker->create(16, 9, _gfx->_texturePixelFormat);
+	_amigaCompassPitchMarker->fillRect(Common::Rect(0, 0, 16, 9), transparent);
+	decodeAmigaSprite(file, _amigaCompassPitchMarker, amigaProgToFile(0x27AC6), 1, 9, palette);
+}
+
 void DarkEngine::loadJetpackRawFrames(Common::SeekableReadStream *file) {
 	// The executable stream still includes the 0x1C-byte GEMDOS header, so the
 	// original program addresses need to be converted back to file offsets here.
-	static const int kGemdosHeaderSize = 0x1C;
 	// Original Amiga layout:
 	// - transition strip at prog 0x23B9E, 9 frames, stride 0x160
 	// - crouch frame at prog 0x2481E
-	static const int kTransitionBaseOffset = 0x23B9E + kGemdosHeaderSize;
+	static const int kTransitionBaseOffset = 0x23B9E + kAmigaGemdosHeaderSize;
 	static const int kTransitionFrameCount = 9;
-	static const int kCrouchFrameOffset = 0x2481E + kGemdosHeaderSize;
+	static const int kCrouchFrameOffset = 0x2481E + kAmigaGemdosHeaderSize;
 	static const int kFrameSize = 0x160; // 2 word columns * 22 rows * 8 bytes/row
 	_jetpackTransitionFrames.clear();
 	for (int i = 0; i < kTransitionFrameCount; i++) {
@@ -246,6 +378,50 @@ void DarkEngine::drawJetpackIndicator(Graphics::Surface *surface) {
 	}
 }
 
+void DarkEngine::drawAmigaCompass(Graphics::Surface *surface) {
+	uint32 transparent = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0, 0, 0);
+
+	if (!_amigaCompassYawFrames.empty()) {
+		float yaw = _yaw;
+		while (yaw < 0.0f)
+			yaw += 360.0f;
+		while (yaw >= 360.0f)
+			yaw -= 360.0f;
+
+		int frame = ((int)(yaw / 5.0f)) % _amigaCompassYawFrames.size();
+		surface->copyRectToSurfaceWithKey(*_amigaCompassYawFrames[frame], 48, 15,
+			Common::Rect(_amigaCompassYawFrames[frame]->w, _amigaCompassYawFrames[frame]->h), transparent);
+	}
+
+	if (_amigaCompassPitchMarker) {
+		int pos = CLIP<int>((int)(_pitch / 1.65f), -36, 36);
+		surface->copyRectToSurfaceWithKey(*_amigaCompassPitchMarker, 304, 94 + pos,
+			Common::Rect(_amigaCompassPitchMarker->w, _amigaCompassPitchMarker->h), transparent);
+	}
+}
+
+void DarkEngine::drawAmigaAmbientIndicators(Graphics::Surface *surface) {
+	uint32 transparent = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0, 0, 0);
+
+	if (!_amigaCompassNeedleFrames.empty()) {
+		int frame = (_ticks / 4) % _amigaCompassNeedleFrames.size();
+		surface->copyRectToSurfaceWithKey(*_amigaCompassNeedleFrames[frame], 208, 21,
+			Common::Rect(_amigaCompassNeedleFrames[frame]->w, _amigaCompassNeedleFrames[frame]->h), transparent);
+	}
+
+	if (!_amigaCompassLeftFrames.empty()) {
+		int frame = (_ticks / 5) % _amigaCompassLeftFrames.size();
+		surface->copyRectToSurfaceWithKey(*_amigaCompassLeftFrames[frame], 0, 143,
+			Common::Rect(_amigaCompassLeftFrames[frame]->w, _amigaCompassLeftFrames[frame]->h), transparent);
+	}
+
+	if (!_amigaCompassRightFrames.empty()) {
+		int frame = (_ticks / 5) % _amigaCompassRightFrames.size();
+		surface->copyRectToSurfaceWithKey(*_amigaCompassRightFrames[frame], 288, 143,
+			Common::Rect(_amigaCompassRightFrames[frame]->w, _amigaCompassRightFrames[frame]->h), transparent);
+	}
+}
+
 void DarkEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 	uint32 white = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0xFF, 0xFF, 0xFF);
 	uint32 yellow = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0xEE, 0xCC, 0x00);
@@ -281,6 +457,8 @@ void DarkEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 	drawString(kDarkFontSmall, _currentArea->_name, 32, 151, grey8, greyA, transparent, surface);
 	drawBinaryClock(surface, 6, 110, white, grey);
 
+	drawAmigaCompass(surface);
+	drawAmigaAmbientIndicators(surface);
 	drawJetpackIndicator(surface);
 
 	int x = 229;
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index 0bfcc5181cb..6fdd7f32914 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -90,6 +90,7 @@ DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEn
 	_initialShield = 15;
 
 	_jetFuelSeconds = _initialEnergy * 6;
+	_amigaCompassPitchMarker = nullptr;
 	_jetpackIndicatorStateInitialized = false;
 	_jetpackIndicatorLastFlyMode = false;
 	_jetpackIndicatorTransitionFrame = 0;
diff --git a/engines/freescape/games/dark/dark.h b/engines/freescape/games/dark/dark.h
index c1a0d3c11ba..94b618b5d46 100644
--- a/engines/freescape/games/dark/dark.h
+++ b/engines/freescape/games/dark/dark.h
@@ -111,12 +111,21 @@ public:
 	// hardcoded palette at draw time.
 	Common::Array<Common::Array<byte>> _jetpackTransitionFrames;
 	Common::Array<byte> _jetpackCrouchFrame;
+	Common::Array<Graphics::ManagedSurface *> _amigaCompassYawFrames;
+	Graphics::ManagedSurface *_amigaCompassPitchMarker;
+	Common::Array<Graphics::ManagedSurface *> _amigaCompassNeedleFrames;
+	Common::Array<Graphics::ManagedSurface *> _amigaCompassLeftFrames;
+	Common::Array<Graphics::ManagedSurface *> _amigaCompassRightFrames;
 	bool _jetpackIndicatorStateInitialized;
 	bool _jetpackIndicatorLastFlyMode;
 	int _jetpackIndicatorTransitionFrame;
 	int _jetpackIndicatorTransitionDirection;
 	uint32 _jetpackIndicatorNextFrameMillis;
 	void loadJetpackRawFrames(Common::SeekableReadStream *file);
+	void loadAmigaIndicatorSprites(Common::SeekableReadStream *file, byte *palette);
+	void loadAmigaCompass(Common::SeekableReadStream *file, byte *palette);
+	void drawAmigaCompass(Graphics::Surface *surface);
+	void drawAmigaAmbientIndicators(Graphics::Surface *surface);
 	void drawJetpackIndicator(Graphics::Surface *surface);
 
 	int _soundIndexRestoreECD;


Commit: b6b11bfb2e40225cb31bf753685b32ad5fb1c6f9
    https://github.com/scummvm/scummvm/commit/b6b11bfb2e40225cb31bf753685b32ad5fb1c6f9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-27T15:15:44+01:00

Commit Message:
FREESCAPE: allow compass to spin in dark amiga

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


diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 367aa650221..95bef14ff53 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -31,13 +31,54 @@ namespace Freescape {
 
 namespace {
 
-static const int kAmigaGemdosHeaderSize = 0x1C;
+const int kAmigaGemdosHeaderSize = 0x1C;
 
-static int amigaProgToFile(int address) {
+int amigaProgToFile(int address) {
 	return address + kAmigaGemdosHeaderSize;
 }
 
-static void decodeMaskedAmigaSprite(Common::SeekableReadStream *file, Graphics::ManagedSurface *surf,
+int wrapCompassPhase(int phase, int frameCount) {
+	if (frameCount <= 0)
+		return 0;
+
+	phase %= frameCount;
+	if (phase < 0)
+		phase += frameCount;
+	return phase;
+}
+
+int darkAmigaForcedCompassStep(int areaId) {
+	switch (areaId) {
+	case 1:
+	case 27:
+	case 28:
+		return 1;
+	case 18:
+		return -1;
+	default:
+		return 0;
+	}
+}
+
+int yawToCompassPhase(float yaw, int frameCount) {
+	while (yaw < 0.0f)
+		yaw += 360.0f;
+	while (yaw >= 360.0f)
+		yaw -= 360.0f;
+
+	return wrapCompassPhase((int)(yaw / 5.0f), frameCount);
+}
+
+int stepCompassPhaseTowardTarget(int current, int target, int frameCount) {
+	if (frameCount <= 0 || current == target)
+		return 0;
+
+	int forwardDistance = wrapCompassPhase(target - current, frameCount);
+	int backwardDistance = wrapCompassPhase(current - target, frameCount);
+	return (forwardDistance < backwardDistance) ? 1 : -1;
+}
+
+void decodeMaskedAmigaSprite(Common::SeekableReadStream *file, Graphics::ManagedSurface *surf,
 		int dataOffset, int widthWords, int height, const uint16 *maskWords,
 		const Graphics::PixelFormat &pixelFormat, const byte *palette) {
 	for (int y = 0; y < height; y++) {
@@ -110,7 +151,7 @@ void DarkEngine::loadAssetsAmigaFullGame() {
 	// Dark Side: COLOR5 palette cycling from assembly interrupt handler at $10E4.
 	// Cycles $DFF18A (COLOR5) every 2 frames through 30 entries.
 	{
-		static const uint16 kDarkSideCyclingTable[] = {
+		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,
@@ -137,9 +178,9 @@ void DarkEngine::loadAssetsAmigaFullGame() {
 
 	// Load HDSMUSIC.AM music data (Wally Beben custom engine)
 	// HDSMUSIC.AM is an embedded GEMDOS executable at stream offset $BA64
-	static const uint32 kHdsMusicOffset = 0xBA64;
-	static const uint32 kGemdosHeaderSize = 0x1C;
-	static const uint32 kHdsMusicTextSize = 0xF4BC;
+	const uint32 kHdsMusicOffset = 0xBA64;
+	const uint32 kGemdosHeaderSize = 0x1C;
+	const uint32 kHdsMusicTextSize = 0xF4BC;
 
 	stream->seek(kHdsMusicOffset + kGemdosHeaderSize);
 	_musicData.resize(kHdsMusicTextSize);
@@ -185,9 +226,9 @@ void DarkEngine::loadAmigaIndicatorSprites(Common::SeekableReadStream *file, byt
 		_amigaCompassNeedleFrames.push_back(surf);
 	}
 
-	static const uint16 kLeftMasks[2] = { 0xE000, 0x01FF };
-	static const uint16 kRightMasks[2] = { 0xFF80, 0x003F };
-	static const int kSideFrameOrder[4] = { 3, 2, 1, 0 };
+	const uint16 kLeftMasks[2] = { 0xE000, 0x01FF };
+	const uint16 kRightMasks[2] = { 0xFF80, 0x003F };
+	const int kSideFrameOrder[4] = { 3, 2, 1, 0 };
 
 	_amigaCompassLeftFrames.clear();
 	for (int phase = 0; phase < 4; phase++) {
@@ -264,10 +305,10 @@ void DarkEngine::loadJetpackRawFrames(Common::SeekableReadStream *file) {
 	// Original Amiga layout:
 	// - transition strip at prog 0x23B9E, 9 frames, stride 0x160
 	// - crouch frame at prog 0x2481E
-	static const int kTransitionBaseOffset = 0x23B9E + kAmigaGemdosHeaderSize;
-	static const int kTransitionFrameCount = 9;
-	static const int kCrouchFrameOffset = 0x2481E + kAmigaGemdosHeaderSize;
-	static const int kFrameSize = 0x160; // 2 word columns * 22 rows * 8 bytes/row
+	const int kTransitionBaseOffset = 0x23B9E + kAmigaGemdosHeaderSize;
+	const int kTransitionFrameCount = 9;
+	const int kCrouchFrameOffset = 0x2481E + kAmigaGemdosHeaderSize;
+	const int kFrameSize = 0x160; // 2 word columns * 22 rows * 8 bytes/row
 	_jetpackTransitionFrames.clear();
 	for (int i = 0; i < kTransitionFrameCount; i++) {
 		file->seek(kTransitionBaseOffset + i * kFrameSize);
@@ -285,18 +326,18 @@ void DarkEngine::loadJetpackRawFrames(Common::SeekableReadStream *file) {
 }
 
 void DarkEngine::drawJetpackIndicator(Graphics::Surface *surface) {
-	static const int kTransitionFrameCount = 9;
-	static const uint32 kFrameDelayMs = 60;
-	static const int kVisibleLeftX = 109;
-	static const int kSourceLeftPadding = 13;
-	static const int kDrawBaseX = kVisibleLeftX - kSourceLeftPadding;
-	static const int kHeight = 22;
-	static const int kWidthWords = 2;
-	static const int kDrawY = 175;
-	static const uint16 kMaskWords[kWidthWords] = { 0xFFF8, 0x00FF };
-	static const int kFlyingBaseFrame = 0;
-	static const int kGroundStandingFrame = 8;
-	static const uint16 kJetpackColors[16] = {
+	const int kTransitionFrameCount = 9;
+	const uint32 kFrameDelayMs = 60;
+	const int kVisibleLeftX = 109;
+	const int kSourceLeftPadding = 13;
+	const int kDrawBaseX = kVisibleLeftX - kSourceLeftPadding;
+	const int kHeight = 22;
+	const int kWidthWords = 2;
+	const int kDrawY = 175;
+	const uint16 kMaskWords[kWidthWords] = { 0xFFF8, 0x00FF };
+	const int kFlyingBaseFrame = 0;
+	const int kGroundStandingFrame = 8;
+	const uint16 kJetpackColors[16] = {
 		0x000, 0x222, 0x000, 0x000,
 		0x000, 0x000, 0x444, 0x666,
 		0x000, 0x800, 0xA00, 0xF00,
@@ -382,13 +423,24 @@ void DarkEngine::drawAmigaCompass(Graphics::Surface *surface) {
 	uint32 transparent = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0, 0, 0);
 
 	if (!_amigaCompassYawFrames.empty()) {
-		float yaw = _yaw;
-		while (yaw < 0.0f)
-			yaw += 360.0f;
-		while (yaw >= 360.0f)
-			yaw -= 360.0f;
+		const int frameCount = _amigaCompassYawFrames.size();
+		const int targetPhase = yawToCompassPhase(_yaw, frameCount);
+		if (!_amigaCompassYawPhaseInitialized) {
+			_amigaCompassYawPhaseInitialized = true;
+			_amigaCompassYawPhase = targetPhase;
+			_amigaCompassYawLastUpdateTick = _ticks;
+		} else if (_amigaCompassYawLastUpdateTick != _ticks) {
+			int step = 0;
+			if (_currentArea)
+				step = darkAmigaForcedCompassStep(_currentArea->getAreaID());
+			if (step == 0)
+				step = stepCompassPhaseTowardTarget(_amigaCompassYawPhase, targetPhase, frameCount);
+
+			_amigaCompassYawPhase = wrapCompassPhase(_amigaCompassYawPhase + step, frameCount);
+			_amigaCompassYawLastUpdateTick = _ticks;
+		}
 
-		int frame = ((int)(yaw / 5.0f)) % _amigaCompassYawFrames.size();
+		const int frame = wrapCompassPhase(_amigaCompassYawPhase, frameCount);
 		surface->copyRectToSurfaceWithKey(*_amigaCompassYawFrames[frame], 48, 15,
 			Common::Rect(_amigaCompassYawFrames[frame]->w, _amigaCompassYawFrames[frame]->h), transparent);
 	}
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index 6fdd7f32914..d1115ad8045 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -91,6 +91,9 @@ DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEn
 
 	_jetFuelSeconds = _initialEnergy * 6;
 	_amigaCompassPitchMarker = nullptr;
+	_amigaCompassYawPhaseInitialized = false;
+	_amigaCompassYawPhase = 0;
+	_amigaCompassYawLastUpdateTick = -1;
 	_jetpackIndicatorStateInitialized = false;
 	_jetpackIndicatorLastFlyMode = false;
 	_jetpackIndicatorTransitionFrame = 0;
@@ -308,6 +311,9 @@ void DarkEngine::initGameState() {
 
 	_angleRotationIndex = 0;
 	_playerStepIndex = 6;
+	_amigaCompassYawPhaseInitialized = false;
+	_amigaCompassYawPhase = 0;
+	_amigaCompassYawLastUpdateTick = -1;
 	_jetpackIndicatorStateInitialized = false;
 	_jetpackIndicatorTransitionDirection = 0;
 
@@ -1067,6 +1073,9 @@ Common::Error DarkEngine::loadGameStreamExtended(Common::SeekableReadStream *str
 		uint16 key = stream->readUint16LE();
 		_exploredAreas[key] = stream->readUint32LE();
 	}
+	_amigaCompassYawPhaseInitialized = false;
+	_amigaCompassYawPhase = 0;
+	_amigaCompassYawLastUpdateTick = -1;
 	_jetpackIndicatorStateInitialized = false;
 	_jetpackIndicatorTransitionDirection = 0;
 	return Common::kNoError;
diff --git a/engines/freescape/games/dark/dark.h b/engines/freescape/games/dark/dark.h
index 94b618b5d46..5a030a6d847 100644
--- a/engines/freescape/games/dark/dark.h
+++ b/engines/freescape/games/dark/dark.h
@@ -116,6 +116,9 @@ public:
 	Common::Array<Graphics::ManagedSurface *> _amigaCompassNeedleFrames;
 	Common::Array<Graphics::ManagedSurface *> _amigaCompassLeftFrames;
 	Common::Array<Graphics::ManagedSurface *> _amigaCompassRightFrames;
+	bool _amigaCompassYawPhaseInitialized;
+	int _amigaCompassYawPhase;
+	int _amigaCompassYawLastUpdateTick;
 	bool _jetpackIndicatorStateInitialized;
 	bool _jetpackIndicatorLastFlyMode;
 	int _jetpackIndicatorTransitionFrame;


Commit: 14ae29f3ba5999d8e4606d335bb07f90282d56b8
    https://github.com/scummvm/scummvm/commit/14ae29f3ba5999d8e4606d335bb07f90282d56b8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-27T16:03:46+01:00

Commit Message:
FREESCAPE: eclipse heartbeat fixes

Changed paths:
    engines/freescape/freescape.cpp
    engines/freescape/freescape.h
    engines/freescape/games/eclipse/eclipse.cpp
    engines/freescape/games/eclipse/eclipse.h
    engines/freescape/ui.cpp


diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 02902bd8f0f..400b9187568 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -206,6 +206,7 @@ FreescapeEngine::FreescapeEngine(OSystem *syst, const ADGameDescription *gd)
 	_fontLoaded = false;
 	_dataBundle = nullptr;
 	_extraBuffer = nullptr;
+	_inWaitLoop = false;
 
 	_lastFrame = 0;
 	// The near clip plane of 2 is useful for Driller and Dark Side as they have open spaces without too much
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 499b61615be..d3ccc01fae9 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -649,6 +649,7 @@ public:
 	int _ticksFromEnd;
 	int _lastTick;
 	int _lastMinute;
+	bool _inWaitLoop;
 
 	void getTimeFromCountdown(int &seconds, int &minutes, int &hours);
 	virtual void updateTimeVariables();
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 290b8810fa2..7ebd1105b91 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -92,6 +92,8 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
 
 	_lastThirtySeconds = 0;
 	_lastFiveSeconds = 0;
+	_lastHeartbeatSoundTick = -1;
+	_lastHeartIndicatorFrame = 1;
 	_lastSecond = -1;
 	_resting = false;
 	_flashlightOn = false;
@@ -109,6 +111,8 @@ void EclipseEngine::initGameState() {
 	getTimeFromCountdown(seconds, minutes, hours);
 	_lastThirtySeconds = seconds / 30;
 	_lastFiveSeconds = seconds / 5;
+	_lastHeartbeatSoundTick = -1;
+	_lastHeartIndicatorFrame = 1;
 	_resting = false;
 	_flashlightOn = false;
 
@@ -809,10 +813,20 @@ void EclipseEngine::drawHeartIndicator(Graphics::Surface *surface, int x, int y)
 	int beatCycle = MAX(shield, 1);
 	int phase = _ticks % beatCycle;
 	int beatStart = MAX(beatCycle - 5, 0);
-	int frame = (phase >= beatStart) ? 0 : 1;
-
-	if (phase == beatStart)
-		playSound(1, false, _soundFxHandle);
+	int frame = _lastHeartIndicatorFrame;
+
+	if (_avoidRenderingFrames > 0 || _hasFallen) {
+		frame = 1;
+		_lastHeartIndicatorFrame = frame;
+	} else if (!_inWaitLoop) {
+		frame = (phase >= beatStart) ? 0 : 1;
+		_lastHeartIndicatorFrame = frame;
+
+		if (!isPaused() && phase == beatStart && _lastHeartbeatSoundTick != _ticks) {
+			playSound(1, false, _soundFxHandle);
+			_lastHeartbeatSoundTick = _ticks;
+		}
+	}
 
 	surface->copyRectToSurface(*_eclipseSprites[frame], x, y,
 		Common::Rect(_eclipseSprites[frame]->w, _eclipseSprites[frame]->h));
@@ -994,6 +1008,8 @@ Common::Error EclipseEngine::saveGameStreamExtended(Common::WriteStream *stream,
 }
 
 Common::Error EclipseEngine::loadGameStreamExtended(Common::SeekableReadStream *stream) {
+	_lastHeartbeatSoundTick = -1;
+	_lastHeartIndicatorFrame = 1;
 	return Common::kNoError;
 }
 
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index 6093b760ef4..5b66b8187fa 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -62,6 +62,8 @@ public:
 	bool _flashlightOn;
 	int _lastThirtySeconds;
 	int _lastFiveSeconds;
+	int _lastHeartbeatSoundTick;
+	int _lastHeartIndicatorFrame;
 
 	int _lastSecond;
 	void updateTimeVariables() override;
diff --git a/engines/freescape/ui.cpp b/engines/freescape/ui.cpp
index 7aa78105b17..421f8bd917a 100644
--- a/engines/freescape/ui.cpp
+++ b/engines/freescape/ui.cpp
@@ -25,6 +25,7 @@ namespace Freescape {
 
 void FreescapeEngine::waitInLoop(int maxWait) {
 	long int startTick = _ticks;
+	_inWaitLoop = true;
 	while (_ticks <= startTick + maxWait) {
 		Common::Event event;
 		while (_eventManager->pollEvent(event)) {
@@ -35,6 +36,7 @@ void FreescapeEngine::waitInLoop(int maxWait) {
 			switch (event.type) {
 			case Common::EVENT_QUIT:
 			case Common::EVENT_RETURN_TO_LAUNCHER:
+				_inWaitLoop = false;
 				quitGame();
 				return;
 
@@ -95,6 +97,7 @@ void FreescapeEngine::waitInLoop(int maxWait) {
 		g_system->updateScreen();
 		g_system->delayMillis(15); // try to target ~60 FPS
 	}
+	_inWaitLoop = false;
 	_gfx->clear(0, 0, 0, true);
 	_eventManager->purgeMouseEvents();
 	_eventManager->purgeKeyboardEvents();
@@ -303,12 +306,21 @@ void FreescapeEngine::drawFullscreenMessageAndWait(Common::String message) {
 
 void FreescapeEngine::drawBorderScreenAndWait(Graphics::Surface *surface, int maxWait) {
 	PauseToken pauseToken = pauseEngine();
+	Graphics::Surface *compositedSurface = nullptr;
+	if (surface)
+		compositedSurface = new Graphics::Surface();
+
 	for (int i = 0; i < maxWait; i++) {
 		Common::Event event;
 		while (_eventManager->pollEvent(event)) {
 			switch (event.type) {
 			case Common::EVENT_QUIT:
 			case Common::EVENT_RETURN_TO_LAUNCHER:
+				if (compositedSurface) {
+					compositedSurface->free();
+					delete compositedSurface;
+				}
+				pauseToken.clear();
 				quitGame();
 				return;
 
@@ -349,14 +361,19 @@ void FreescapeEngine::drawBorderScreenAndWait(Graphics::Surface *surface, int ma
 		_gfx->clear(0, 0, 0, true);
 		drawBorder();
 		if (surface) {
+			compositedSurface->copyFrom(*surface);
 			if (_currentArea)
-				drawPlatformUI(surface);
-			drawFullscreenSurface(surface);
+				drawPlatformUI(compositedSurface);
+			drawFullscreenSurface(compositedSurface);
 		}
 		_gfx->flipBuffer();
 		g_system->updateScreen();
 		g_system->delayMillis(15); // try to target ~60 FPS
 	}
+	if (compositedSurface) {
+		compositedSurface->free();
+		delete compositedSurface;
+	}
 	pauseToken.clear();
 	playSound(_soundIndexMenu, false, _soundFxHandle);
 	_gfx->clear(0, 0, 0, true);




More information about the Scummvm-git-logs mailing list