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

neuromancer noreply at scummvm.org
Mon Feb 23 10:39:50 UTC 2026


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

Summary:
be81ef1f3c FREESCAPE: fixes for castle master cpc
557c3e47ed FREESCAPE: initial code for eclipse music in atari
c67fbca751 FREESCAPE: fixes for eclipse music in atari
c267b8c31f FREESCAPE: improve eclipse ui in atari
8be72a7c1e FREESCAPE: improved eclipse ui in atari
0f68a39da5 FREESCAPE: improved eclipse ui in atari
1d61c84029 FREESCAPE: on-screen control for eclipse in atari
d45d8d20dd FREESCAPE: process indicator in eclipse in atari


Commit: be81ef1f3c7de1a514da354de813cdcf363f0d70
    https://github.com/scummvm/scummvm/commit/be81ef1f3c7de1a514da354de813cdcf363f0d70
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:30+01:00

Commit Message:
FREESCAPE: fixes for castle master cpc

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


diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 31652e5133a..e524e203654 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -733,7 +733,7 @@ void CastleEngine::drawInfoMenu() {
 				keyRects.push_back(Common::Rect(80, y, 80 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
 			}
 		}
-	} else if (isSpectrum()) {
+	} else if (isSpectrum() || isCPC()) {
 		Common::Array<Common::String> lines;
 		lines.push_back(centerAndPadString("********************", 21));
 
@@ -972,7 +972,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
 	Common::String scoreString;
 	if (isDOS())
 		scoreString = _messagesList[131];
-	else if (isSpectrum()) {
+	else if (isSpectrum() || isCPC()) {
 		if (_language == Common::EN_ANY)
 			scoreString = "SCORE XXXXXXX";
 		else if (_language == Common::ES_ESP)
@@ -987,7 +987,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
 	Common::String spiritsDestroyedString;
 	if (isDOS())
 		spiritsDestroyedString = _messagesList[133];
-	else if (isSpectrum()) {
+	else if (isSpectrum() || isCPC()) {
 		if (_language == Common::EN_ANY)
 			spiritsDestroyedString = "X DESTROYED";
 		else if (_language == Common::ES_ESP)
@@ -1488,7 +1488,7 @@ bool CastleEngine::ghostInArea() {
 }
 
 void CastleEngine::drawSensorShoot(Sensor *sensor) {
-	if (isSpectrum()) {
+	if (isSpectrum() || isCPC()) {
 		_gfx->_inkColor = 1 + (_gfx->_inkColor + 1) % 7;
 	} else if (isDOS()) {
 		float shakeIntensity = 10;
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index a451f469965..cd9f8022774 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -370,13 +370,6 @@ void CastleEngine::loadAssetsCPCFullGame() {
 			it._value->addObjectFromArea(id, _areaMap[255]);
 		}
 	}
-	// Discard some global conditions
-	// It is unclear why they hide/unhide objects that formed the spirits
-	for (int i = 0; i < 3; i++) {
-		debugC(kFreescapeDebugParser, "Discarding condition %s", _conditionSources[0].c_str());
-		_conditions.remove_at(0);
-		_conditionSources.remove_at(0);
-	}
 }
 
 void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
@@ -418,7 +411,7 @@ void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
 	}
 
 	// Draw energy meter (strength)
-	drawEnergyMeter(surface, Common::Point(38, 158));
+	drawEnergyMeter(surface, Common::Point(45, 158));
 
 	// Draw spirit meter
 	uint32 blackColor = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
@@ -439,7 +432,7 @@ void CastleEngine::drawCPCUI(Graphics::Surface *surface) {
 	if (!_flagFrames.empty()) {
 		int ticks = g_system->getMillis() / 20;
 		int flagFrameIndex = (ticks / 10) % 4;
-		surface->copyRectToSurface(*_flagFrames[flagFrameIndex], 285, 5, Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));
+		surface->copyRectToSurface(*_flagFrames[flagFrameIndex], 300, 4, Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));
 	}
 
 }


Commit: 557c3e47ede2af1cae45b24748267411fbac8b6b
    https://github.com/scummvm/scummvm/commit/557c3e47ede2af1cae45b24748267411fbac8b6b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:30+01:00

Commit Message:
FREESCAPE: initial code for eclipse music in atari

Changed paths:
  A engines/freescape/games/eclipse/atari.music.cpp
    engines/freescape/games/eclipse/atari.cpp
    engines/freescape/games/eclipse/eclipse.cpp
    engines/freescape/games/eclipse/eclipse.h
    engines/freescape/module.mk


diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 67af6cdf4ca..1ad82118ed6 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -174,6 +174,15 @@ void EclipseEngine::loadAssetsAtariFullGame() {
 	loadPalettes(stream, 0x2a0fa);
 	loadSoundsFx(stream, 0x3030c, 6);
 
+	// Load TEMUSIC.ST (GEMDOS executable at file offset $11F5A, skip $1C header, TEXT size $11E8)
+	static const uint32 kTEMusicOffset = 0x11F5A;
+	static const uint32 kGemdosHeaderSize = 0x1C;
+	static const uint32 kTEMusicTextSize = 0x11E8;
+	stream->seek(kTEMusicOffset + kGemdosHeaderSize);
+	_musicData.resize(kTEMusicTextSize);
+	stream->read(_musicData.data(), kTEMusicTextSize);
+	debug(3, "TE-Atari: Loaded TEMUSIC.ST TEXT segment (%d bytes)", kTEMusicTextSize);
+
 	/*
 	loadFonts(stream, 0xd06b, _fontBig);
 	loadFonts(stream, 0xd49a, _fontMedium);
diff --git a/engines/freescape/games/eclipse/atari.music.cpp b/engines/freescape/games/eclipse/atari.music.cpp
new file mode 100644
index 00000000000..c342bdbb794
--- /dev/null
+++ b/engines/freescape/games/eclipse/atari.music.cpp
@@ -0,0 +1,788 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * Total Eclipse Atari ST music player (YM2149 PSG).
+ *
+ * Plays background music from the TEMUSIC.ST embedded GEMDOS executable.
+ * Uses the same Wally Beben byte-stream pattern format as the Amiga
+ * Dark Side engine (wb.cpp), but outputs to the YM2149/AY-3-8912 PSG
+ * instead of Amiga Paula.
+ *
+ * TEMUSIC.ST data table offsets (TEXT-relative):
+ *   $0B24  Period table (96 x uint16 BE)
+ *   $0CC8  Arpeggio interval lookup (8 bytes)
+ *   $0D60  Instrument table (12 x 8 bytes)
+ *   $0DC0  Song table (2 songs x 3 channels x uint32 BE order-list pointers)
+ *   $0DCC  Pattern pointer table (up to 31 x uint32 BE)
+ */
+
+#include "audio/softsynth/ay8912.h"
+
+#include "freescape/freescape.h"
+
+#include "common/endian.h"
+#include "common/debug.h"
+#include "common/util.h"
+
+namespace Freescape {
+
+// TEXT-relative offsets for data tables within TEMUSIC.ST
+static const uint32 kTEPeriodTableOffset      = 0x0B24; // 96 x uint16 BE
+static const uint32 kTEArpeggioIntervalsOffset = 0x0CC8; // 8 bytes
+static const uint32 kTEInstrumentTableOffset   = 0x0D60; // 12 x 8 bytes
+static const uint32 kTESongTableOffset         = 0x0DC0; // 2 songs x 3 ch x uint32 BE
+static const uint32 kTEPatternPtrTableOffset   = 0x0DCC; // up to 31 x uint32 BE
+
+static const int kTENumChannels    = 3;
+static const int kTENumPeriods     = 96;
+static const int kTENumInstruments = 12;
+static const int kTEMaxPatterns    = 31;
+
+class EclipseAtariMusicStream : public Audio::AY8912Stream {
+public:
+	EclipseAtariMusicStream(const byte *data, uint32 dataSize, int songNum, int rate = 44100);
+	~EclipseAtariMusicStream() override {}
+
+	int readBuffer(int16 *buffer, const int numSamples) override;
+	bool endOfData() const override { return !_musicActive; }
+	bool endOfStream() const override { return !_musicActive; }
+
+private:
+	// --- Data tables ---
+	const byte *_data;
+	uint32 _dataSize;
+
+	uint16 _periods[kTENumPeriods];
+
+	struct InstrumentDesc {
+		byte volume;       // Initial volume (0-$3F)
+		byte targetVol;    // Sustain/target volume
+		byte attackRate;   // Volume increment per tick
+		byte releaseRate;  // Volume decrement per tick
+		byte envFlags;     // Bit 7: hardware envelope
+		byte effectType;   // Effect configuration
+		byte arpeggioData; // Arpeggio bit pattern
+		byte flags;        // Additional flags
+	};
+	InstrumentDesc _instruments[kTENumInstruments];
+
+	// Song order list pointers (TEXT-relative)
+	uint32 _songOrderPtrs[2][kTENumChannels];
+
+	// Pattern pointer table
+	uint32 _patternPtrs[kTEMaxPatterns];
+	uint32 _numPatterns;
+
+	// Arpeggio interval lookup
+	byte _arpeggioIntervals[8];
+
+	// --- Per-channel state ---
+	struct ChannelState {
+		// Order list
+		uint32 orderListOffset;
+		int orderListPos;
+		int8 transpose;
+
+		// Current pattern
+		uint32 patternOffset;
+		int patternPos;
+
+		// Note state
+		byte note;
+		byte prevNote;
+		byte duration;
+		int durationCounter;
+
+		// Instrument
+		byte instrumentIdx;
+
+		// Volume envelope
+		byte volume;       // Current volume (0-63 internal scale)
+		byte attackLevel;  // Initial volume on note-on
+		byte decayTarget;  // Target volume to hold
+		byte attackRate;   // Increment per tick
+		byte releaseRate;  // Decrement per tick
+		byte envelopePhase; // 0=attack, 1=decay, 2=sustain, 3=release
+
+		// Effects
+		byte effectMode;   // 0=none, 1=porta/arp
+		bool portaUp;
+		bool portaDown;
+		int16 portaStep;
+		int16 portaTarget;
+		byte arpeggioMask;
+		byte arpeggioPos;
+
+		// Period
+		int16 basePeriod;
+		int16 outputPeriod;
+
+		bool active;
+	};
+	ChannelState _channels[kTENumChannels];
+
+	// --- Global state ---
+	bool _musicActive;
+	byte _tickSpeed;
+	byte _tickCounter;
+	int _tickSampleCount;
+
+	// Arpeggio working table
+	byte _arpeggioTable[16];
+	int _arpeggioTableLen;
+
+	// --- Methods ---
+	void loadTables();
+	void startSong(int songNum);
+	void initChannel(int ch);
+	void readOrderList(int ch);
+	void readPatternCommands(int ch);
+	void triggerNote(int ch);
+	void processEffects(int ch);
+	void processEnvelope(int ch);
+	void buildArpeggioTable(byte mask);
+	void tickUpdate();
+	void writeYMRegisters();
+
+	uint16 getPeriod(int note) const {
+		if (note < 0 || note >= kTENumPeriods)
+			return 0;
+		return _periods[note];
+	}
+
+	byte readDataByte(uint32 offset) const {
+		if (offset < _dataSize)
+			return _data[offset];
+		return 0;
+	}
+
+	uint16 readDataWord(uint32 offset) const {
+		if (offset + 1 < _dataSize)
+			return READ_BE_UINT16(_data + offset);
+		return 0;
+	}
+
+	uint32 readDataLong(uint32 offset) const {
+		if (offset + 3 < _dataSize)
+			return READ_BE_UINT32(_data + offset);
+		return 0;
+	}
+};
+
+// ---------------------------------------------------------------------------
+// Construction / data loading
+// ---------------------------------------------------------------------------
+
+EclipseAtariMusicStream::EclipseAtariMusicStream(const byte *data, uint32 dataSize,
+                                                   int songNum, int rate)
+	: AY8912Stream(rate, 2000000), // YM2149 at 2 MHz on Atari ST
+	  _data(data), _dataSize(dataSize),
+	  _musicActive(false), _tickSpeed(6), _tickCounter(0),
+	  _tickSampleCount(0),
+	  _arpeggioTableLen(0), _numPatterns(0) {
+
+	memset(_periods, 0, sizeof(_periods));
+	memset(_instruments, 0, sizeof(_instruments));
+	memset(_songOrderPtrs, 0, sizeof(_songOrderPtrs));
+	memset(_patternPtrs, 0, sizeof(_patternPtrs));
+	memset(_arpeggioIntervals, 0, sizeof(_arpeggioIntervals));
+	memset(_channels, 0, sizeof(_channels));
+	memset(_arpeggioTable, 0, sizeof(_arpeggioTable));
+
+	// Reset all YM registers
+	for (int r = 0; r < 14; r++)
+		setReg(r, 0);
+	// Mixer: all channels disabled initially
+	setReg(7, 0x3F);
+
+	loadTables();
+	startSong(songNum);
+}
+
+void EclipseAtariMusicStream::loadTables() {
+	// Period table: 96 x uint16 BE at TEXT+$0B24
+	for (int i = 0; i < kTENumPeriods; i++) {
+		_periods[i] = readDataWord(kTEPeriodTableOffset + i * 2);
+	}
+
+	// Arpeggio interval table: 8 bytes at TEXT+$0CC8
+	for (int i = 0; i < 8; i++) {
+		_arpeggioIntervals[i] = readDataByte(kTEArpeggioIntervalsOffset + i);
+	}
+
+	// Instrument table: 12 x 8 bytes at TEXT+$0D60
+	for (int i = 0; i < kTENumInstruments; i++) {
+		uint32 off = kTEInstrumentTableOffset + i * 8;
+		_instruments[i].volume      = readDataByte(off + 0);
+		_instruments[i].targetVol   = readDataByte(off + 1);
+		_instruments[i].attackRate  = readDataByte(off + 2);
+		_instruments[i].releaseRate = readDataByte(off + 3);
+		_instruments[i].envFlags    = readDataByte(off + 4);
+		_instruments[i].effectType  = readDataByte(off + 5);
+		_instruments[i].arpeggioData = readDataByte(off + 6);
+		_instruments[i].flags       = readDataByte(off + 7);
+	}
+
+	// Song table: 2 songs x 3 channels x uint32 BE at TEXT+$0DC0
+	for (int s = 0; s < 2; s++) {
+		for (int ch = 0; ch < kTENumChannels; ch++) {
+			_songOrderPtrs[s][ch] = readDataLong(kTESongTableOffset + s * 12 + ch * 4);
+		}
+	}
+
+	// Pattern pointer table at TEXT+$0DCC
+	_numPatterns = 0;
+	for (uint32 i = 0; i < kTEMaxPatterns; i++) {
+		uint32 ptr = readDataLong(kTEPatternPtrTableOffset + i * 4);
+		_patternPtrs[i] = ptr;
+		if (ptr > 0 && ptr < _dataSize)
+			_numPatterns = i + 1;
+	}
+
+	debug(3, "TE-Atari: Loaded music data (%u bytes)", _dataSize);
+	debug(3, "TE-Atari: %d valid patterns", _numPatterns);
+
+	for (int s = 0; s < 2; s++) {
+		debug(3, "TE-Atari: Song %d order ptrs: $%X $%X $%X",
+			s + 1, _songOrderPtrs[s][0], _songOrderPtrs[s][1], _songOrderPtrs[s][2]);
+	}
+
+	for (int i = 0; i < kTENumInstruments; i++) {
+		const InstrumentDesc &inst = _instruments[i];
+		if (inst.volume > 0 || inst.targetVol > 0)
+			debug(3, "TE-Atari: Inst %d: vol=%d target=%d atk=%d rel=%d envFlags=$%02X effect=$%02X arp=$%02X flags=$%02X",
+				i, inst.volume, inst.targetVol, inst.attackRate, inst.releaseRate,
+				inst.envFlags, inst.effectType, inst.arpeggioData, inst.flags);
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Song init
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::startSong(int songNum) {
+	_musicActive = false;
+
+	if (songNum < 1 || songNum > 2)
+		return;
+
+	int songIdx = songNum - 1;
+	_tickSpeed = 6;
+	_tickCounter = 0;
+	_arpeggioTableLen = 0;
+
+	// Silence all YM channels
+	for (int r = 0; r < 14; r++)
+		setReg(r, 0);
+	setReg(7, 0x3F); // All disabled
+
+	for (int ch = 0; ch < kTENumChannels; ch++) {
+		initChannel(ch);
+		_channels[ch].orderListOffset = _songOrderPtrs[songIdx][ch];
+		_channels[ch].orderListPos = 0;
+		_channels[ch].active = true;
+		readOrderList(ch);
+	}
+
+	_musicActive = true;
+
+	debug(3, "TE-Atari: Song %d started, tickSpeed=%d", songNum, _tickSpeed);
+	for (int ch = 0; ch < kTENumChannels; ch++) {
+		debug(3, "TE-Atari: ch%d orderList=$%X pattern=$%X",
+			ch, _channels[ch].orderListOffset, _channels[ch].patternOffset);
+	}
+}
+
+void EclipseAtariMusicStream::initChannel(int ch) {
+	ChannelState &c = _channels[ch];
+	memset(&c, 0, sizeof(ChannelState));
+	c.duration = 1;
+	c.durationCounter = 0;
+	c.envelopePhase = 3; // Start in release (silent)
+	c.attackLevel = 0x36; // Default from instrument 1
+	c.decayTarget = 0x36;
+}
+
+// ---------------------------------------------------------------------------
+// Order list reader
+// Same format as wb.cpp: $00-$C0=pattern#, $C1-$FE=transpose, $FF=loop
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::readOrderList(int ch) {
+	ChannelState &c = _channels[ch];
+
+	for (int safety = 0; safety < 256; safety++) {
+		if (c.orderListOffset + c.orderListPos >= _dataSize)
+			break;
+
+		byte cmd = readDataByte(c.orderListOffset + c.orderListPos);
+		c.orderListPos++;
+
+		if (cmd == 0xFF) {
+			c.orderListPos = 0;
+			continue;
+		}
+
+		if (cmd > 0xC0) {
+			c.transpose = (int8)((cmd + 0x20) & 0xFF);
+			continue;
+		}
+
+		if (cmd < kTEMaxPatterns && _patternPtrs[cmd] > 0 && _patternPtrs[cmd] < _dataSize) {
+			c.patternOffset = _patternPtrs[cmd];
+			c.patternPos = 0;
+			debugC(3, kFreescapeDebugParser, "TE-Atari: ch%d order -> pattern %d (offset $%04X)", ch, cmd, c.patternOffset);
+		} else {
+			warning("TE-Atari: ch%d pattern index %d invalid", ch, cmd);
+			c.patternOffset = _patternPtrs[0];
+			c.patternPos = 0;
+		}
+		return;
+	}
+
+	warning("TE-Atari: ch%d order list safety limit hit", ch);
+}
+
+// ---------------------------------------------------------------------------
+// Pattern command reader
+// Same format as wb.cpp: $FF=end-pattern, $FE=end-song, $F0+=speed,
+//   $C0+=instrument, $80+=duration, $7F/$7E=portamento,
+//   $7D/$7C=vibrato/arpeggio, $00-$5F=note
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::readPatternCommands(int ch) {
+	ChannelState &c = _channels[ch];
+
+	for (int safety = 0; safety < 256; safety++) {
+		if (c.patternOffset + c.patternPos >= _dataSize)
+			break;
+
+		byte cmd = readDataByte(c.patternOffset + c.patternPos);
+		c.patternPos++;
+
+		if (cmd == 0xFF) {
+			readOrderList(ch);
+			continue;
+		}
+
+		if (cmd == 0xFE) {
+			_musicActive = false;
+			return;
+		}
+
+		if (cmd == 0xFC) {
+			// Song change command — read parameter, restart
+			byte newSong = readDataByte(c.patternOffset + c.patternPos);
+			c.patternPos++;
+			if (newSong >= 1 && newSong <= 2) {
+				startSong(newSong);
+			}
+			return;
+		}
+
+		if (cmd >= 0xF0) {
+			_tickSpeed = cmd & 0x0F;
+			if (_tickSpeed == 0)
+				_tickSpeed = 1;
+			continue;
+		}
+
+		if (cmd >= 0xC0) {
+			// Instrument select
+			// The TEMUSIC.ST encoding: (cmd & $1F) selects envelope/instrument
+			// The instrument index is encoded as (cmd & $1F) >> 3 for the
+			// instrument table. However, looking at the disassembly:
+			// AND $1F, ASL 3 → stored as 8*index in $3C(a0)
+			// Then at $0950: d0 = $3C(a0), lea $0D60, a2 += d0
+			// So the $3C field stores instrumentIdx * 8, and the table
+			// lookup uses it directly as byte offset.
+			// For our purposes: instrument index = (cmd & 0x1F)
+			byte instIdx = cmd & 0x1F;
+			if (instIdx < kTENumInstruments) {
+				c.instrumentIdx = instIdx;
+				const InstrumentDesc &inst = _instruments[instIdx];
+				c.attackLevel = inst.volume;
+				c.decayTarget = inst.targetVol;
+				c.attackRate = inst.attackRate;
+				c.releaseRate = inst.releaseRate;
+
+				if (inst.arpeggioData != 0) {
+					c.effectMode = 1;
+					c.arpeggioMask = inst.arpeggioData;
+					buildArpeggioTable(inst.arpeggioData);
+				}
+			}
+			continue;
+		}
+
+		if (cmd >= 0x80) {
+			c.duration = cmd & 0x3F;
+			if (c.duration == 0)
+				c.duration = 1;
+			continue;
+		}
+
+		if (cmd == 0x7F) {
+			c.portaUp = true;
+			c.portaDown = false;
+			c.effectMode = 1;
+			continue;
+		}
+
+		if (cmd == 0x7E) {
+			c.portaDown = true;
+			c.portaUp = false;
+			c.effectMode = 1;
+			continue;
+		}
+
+		if (cmd == 0x7D) {
+			byte param = readDataByte(c.patternOffset + c.patternPos);
+			c.patternPos++;
+			c.effectMode = 1;
+			c.arpeggioMask = param;
+			buildArpeggioTable(param);
+			continue;
+		}
+
+		if (cmd == 0x7C) {
+			byte param = readDataByte(c.patternOffset + c.patternPos);
+			c.patternPos++;
+			c.effectMode = 1;
+			c.arpeggioMask = param;
+			buildArpeggioTable(param);
+			continue;
+		}
+
+		if (cmd == 0x7A) {
+			// Delay command — skip parameter byte (same as wb.cpp)
+			c.patternPos++;
+			continue;
+		}
+
+		// Note value ($00-$5F)
+		c.prevNote = c.note;
+		c.note = cmd;
+		c.durationCounter = c.duration;
+		triggerNote(ch);
+		return;
+	}
+
+	warning("TE-Atari: ch%d pattern read safety limit hit", ch);
+}
+
+// ---------------------------------------------------------------------------
+// Note trigger — set YM period, reset envelope
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::triggerNote(int ch) {
+	ChannelState &c = _channels[ch];
+
+	if (c.note == 0) {
+		// Rest — silence channel
+		c.outputPeriod = 0;
+		c.volume = 0;
+		c.envelopePhase = 3;
+		return;
+	}
+
+	// Apply transpose and clamp
+	int note = c.note + c.transpose;
+	if (note < 1) note = 1;
+	if (note >= kTENumPeriods) note = kTENumPeriods - 1;
+
+	c.basePeriod = getPeriod(note);
+	c.outputPeriod = c.basePeriod;
+
+	if (c.basePeriod == 0) {
+		warning("TE-Atari: ch%d note %d has period 0", ch, note);
+		return;
+	}
+
+	// Reset envelope
+	c.envelopePhase = 0;
+	c.volume = c.attackLevel;
+
+	debugC(3, kFreescapeDebugParser, "TE-Atari: ch%d NOTE note=%d(+%d) period=%d inst=%d vol=%d",
+		ch, c.note, c.transpose, c.basePeriod, c.instrumentIdx, c.volume);
+
+	// Set up portamento if active
+	if (c.portaUp || c.portaDown) {
+		int prevNote = c.prevNote + c.transpose;
+		if (prevNote < 1) prevNote = 1;
+		if (prevNote >= kTENumPeriods) prevNote = kTENumPeriods - 1;
+
+		int16 prevPeriod = (c.prevNote > 0) ? getPeriod(prevNote) : c.basePeriod;
+		int16 delta = ABS(c.basePeriod - prevPeriod);
+		int steps = (_tickSpeed > 0) ? _tickSpeed : 1;
+		c.portaStep = delta / steps;
+		if (c.portaStep == 0)
+			c.portaStep = 1;
+		c.portaTarget = c.basePeriod;
+		c.basePeriod = prevPeriod;
+		c.outputPeriod = prevPeriod;
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Effects processing — runs every tick (50 Hz)
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::processEffects(int ch) {
+	ChannelState &c = _channels[ch];
+
+	if (c.effectMode == 0) {
+		c.outputPeriod = c.basePeriod;
+		return;
+	}
+
+	if (c.portaUp) {
+		c.basePeriod -= c.portaStep;
+		if (c.basePeriod <= c.portaTarget) {
+			c.basePeriod = c.portaTarget;
+			c.portaUp = false;
+			c.effectMode = 0;
+		}
+		c.outputPeriod = c.basePeriod;
+		return;
+	}
+
+	if (c.portaDown) {
+		c.basePeriod += c.portaStep;
+		if (c.basePeriod >= c.portaTarget) {
+			c.basePeriod = c.portaTarget;
+			c.portaDown = false;
+			c.effectMode = 0;
+		}
+		c.outputPeriod = c.basePeriod;
+		return;
+	}
+
+	// Arpeggio
+	if (_arpeggioTableLen > 0) {
+		int note = c.note + c.transpose;
+		int offset = _arpeggioTable[c.arpeggioPos % _arpeggioTableLen];
+		note += offset;
+		if (note < 1) note = 1;
+		if (note >= kTENumPeriods) note = kTENumPeriods - 1;
+		c.outputPeriod = getPeriod(note);
+		c.arpeggioPos++;
+		if (c.arpeggioPos >= _arpeggioTableLen)
+			c.arpeggioPos = 0;
+	} else {
+		c.outputPeriod = c.basePeriod;
+	}
+}
+
+// ---------------------------------------------------------------------------
+// Volume envelope — runs every tick (50 Hz)
+// Volume range: 0-63 internal, written to YM as >>2 (0-15)
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::processEnvelope(int ch) {
+	ChannelState &c = _channels[ch];
+
+	switch (c.envelopePhase) {
+	case 0: // Attack — volume set to attackLevel in triggerNote
+		if (c.attackRate == 0) {
+			c.volume = c.decayTarget;
+			c.envelopePhase = 2;
+		} else {
+			c.envelopePhase = 1;
+		}
+		break;
+
+	case 1: // Decay — fade toward target
+		if (c.volume < c.decayTarget) {
+			c.volume++;
+		} else if (c.volume > c.decayTarget) {
+			c.volume--;
+		} else {
+			c.envelopePhase = 2;
+		}
+		break;
+
+	case 2: // Sustain — hold
+		break;
+
+	case 3: // Release
+		if (c.releaseRate > 0) {
+			if (c.volume > c.releaseRate) {
+				c.volume -= c.releaseRate;
+			} else {
+				c.volume = 0;
+			}
+		} else {
+			c.volume = 0;
+		}
+		break;
+	}
+
+	if (c.volume > 63)
+		c.volume = 63;
+}
+
+// ---------------------------------------------------------------------------
+// Arpeggio table builder
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::buildArpeggioTable(byte mask) {
+	_arpeggioTableLen = 0;
+	_arpeggioTable[_arpeggioTableLen++] = 0; // Base note
+
+	for (int i = 0; i < 8 && _arpeggioTableLen < 16; i++) {
+		if (mask & (1 << i)) {
+			_arpeggioTable[_arpeggioTableLen++] = _arpeggioIntervals[i];
+		}
+	}
+
+	if (_arpeggioTableLen <= 1)
+		_arpeggioTableLen = 0;
+}
+
+// ---------------------------------------------------------------------------
+// Write channel state to YM2149 registers
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::writeYMRegisters() {
+	byte mixer = 0x3F; // Start with all disabled
+
+	for (int ch = 0; ch < kTENumChannels; ch++) {
+		ChannelState &c = _channels[ch];
+
+		if (!c.active || c.outputPeriod == 0 || c.volume == 0) {
+			// Channel silent
+			setReg(8 + ch, 0); // Volume = 0
+			continue;
+		}
+
+		// Enable tone for this channel
+		mixer &= ~(1 << ch);
+
+		// Set tone period (2 registers per channel)
+		uint16 period = (uint16)c.outputPeriod;
+		setReg(ch * 2, period & 0xFF);       // Fine tune
+		setReg(ch * 2 + 1, (period >> 8) & 0x0F); // Coarse tune
+
+		// Set volume (internal 0-63 → YM 0-15)
+		byte ymVol = c.volume >> 2;
+		if (ymVol > 15) ymVol = 15;
+		setReg(8 + ch, ymVol);
+	}
+
+	setReg(7, mixer);
+}
+
+// ---------------------------------------------------------------------------
+// Main tick update — called at 50 Hz
+// ---------------------------------------------------------------------------
+
+void EclipseAtariMusicStream::tickUpdate() {
+	if (!_musicActive)
+		return;
+
+	// Tick speed gating: the 68000 code uses subq.b #1; bpl; reset which
+	// gives a period of (tickSpeed + 1) ticks before the sequencer advances.
+	// Counter counts: tickSpeed → tickSpeed-1 → ... → 0 → -1(wrap&reset)
+	_tickCounter++;
+	bool sequencerTick = false;
+	if (_tickCounter > _tickSpeed) {
+		_tickCounter = 0;
+		sequencerTick = true;
+	}
+
+	if (sequencerTick) {
+		for (int ch = 0; ch < kTENumChannels; ch++) {
+			if (!_channels[ch].active)
+				continue;
+
+			if (_channels[ch].durationCounter > 0) {
+				_channels[ch].durationCounter--;
+			}
+
+			if (_channels[ch].durationCounter == 0) {
+				if (_channels[ch].envelopePhase < 3)
+					_channels[ch].envelopePhase = 3;
+				readPatternCommands(ch);
+			}
+		}
+	}
+
+	// Every tick: process effects and envelope
+	for (int ch = 0; ch < kTENumChannels; ch++) {
+		if (!_channels[ch].active)
+			continue;
+
+		processEffects(ch);
+		processEnvelope(ch);
+	}
+
+	writeYMRegisters();
+}
+
+// ---------------------------------------------------------------------------
+// Audio stream readBuffer — tick at 50 Hz, generate AY samples
+// ---------------------------------------------------------------------------
+
+int EclipseAtariMusicStream::readBuffer(int16 *buffer, const int numSamples) {
+	if (!_musicActive)
+		return 0;
+
+	int samplesGenerated = 0;
+	// AY8912Stream is stereo: 2 int16 values per frame
+	int samplesPerTick = (getRate() / 50) * 2;
+
+	while (samplesGenerated < numSamples && _musicActive) {
+		int remaining = samplesPerTick - _tickSampleCount;
+		int toGenerate = MIN(numSamples - samplesGenerated, remaining);
+
+		if (toGenerate > 0) {
+			generateSamples(buffer + samplesGenerated, toGenerate);
+			samplesGenerated += toGenerate;
+			_tickSampleCount += toGenerate;
+		}
+
+		if (_tickSampleCount >= samplesPerTick) {
+			_tickSampleCount -= samplesPerTick;
+			tickUpdate();
+		}
+	}
+
+	return samplesGenerated;
+}
+
+// ---------------------------------------------------------------------------
+// Factory function
+// ---------------------------------------------------------------------------
+
+Audio::AudioStream *makeEclipseAtariMusicStream(const byte *data, uint32 dataSize,
+                                                  int songNum, int rate) {
+	if (!data || dataSize < 0x1000) {
+		warning("TE-Atari music: invalid data (size %u)", dataSize);
+		return nullptr;
+	}
+
+	EclipseAtariMusicStream *stream = new EclipseAtariMusicStream(data, dataSize, songNum, rate);
+	return stream->toAudioStream();
+}
+
+} // End of namespace Freescape
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index ee4001655ba..f052d25dfe7 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -21,6 +21,9 @@
 
 #include "common/file.h"
 
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+
 #include "backends/keymapper/action.h"
 #include "backends/keymapper/keymap.h"
 #include "backends/keymapper/standard-actions.h"
@@ -32,6 +35,10 @@
 
 namespace Freescape {
 
+// Forward declaration (defined in atari.music.cpp)
+Audio::AudioStream *makeEclipseAtariMusicStream(const byte *data, uint32 dataSize,
+                                                  int songNum = 1, int rate = 44100);
+
 EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
 	// These sounds can be overriden by the class of each platform
 	_soundIndexStartFalling = -1;
@@ -331,6 +338,16 @@ void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
 	if (isAmiga() || isAtariST())
 		_currentArea->_skyColor = 15;
 
+	// Start background music (Atari ST)
+	if (isAtariST() && !_musicData.empty() && !_mixer->isSoundHandleActive(_musicHandle)) {
+		Audio::AudioStream *musicStream = makeEclipseAtariMusicStream(
+			_musicData.data(), _musicData.size(), 1);
+		if (musicStream) {
+			_mixer->playStream(Audio::Mixer::kMusicSoundType,
+				&_musicHandle, musicStream);
+		}
+	}
+
 	resetInput();
 }
 
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index b74d53fa470..5f8f942523d 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -96,6 +96,8 @@ public:
 
 	soundFx *load1bPCM(Common::SeekableReadStream *file, int offset);
 
+	Common::Array<byte> _musicData; // TEMUSIC.ST TEXT segment (Atari ST)
+
 	bool checkIfGameEnded() override;
 	void endGame() override;
 	void loadSoundsFx(Common::SeekableReadStream *file, int offset, int number) override;
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index ea12765c2a4..413a85bfdf8 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -31,6 +31,7 @@ MODULE_OBJS := \
 	games/driller/sounds.o \
 	games/driller/zx.o \
 	games/eclipse/atari.o \
+	games/eclipse/atari.music.o \
 	games/eclipse/c64.o \
 	games/eclipse/dos.o \
 	games/eclipse/eclipse.o \


Commit: c67fbca75160ab8596fc4d1ac009fe21dc03067f
    https://github.com/scummvm/scummvm/commit/c67fbca75160ab8596fc4d1ac009fe21dc03067f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00

Commit Message:
FREESCAPE: fixes for eclipse music in atari

Changed paths:
    engines/freescape/games/eclipse/atari.music.cpp


diff --git a/engines/freescape/games/eclipse/atari.music.cpp b/engines/freescape/games/eclipse/atari.music.cpp
index c342bdbb794..b3518d87c43 100644
--- a/engines/freescape/games/eclipse/atari.music.cpp
+++ b/engines/freescape/games/eclipse/atari.music.cpp
@@ -124,7 +124,7 @@ private:
 		byte envelopePhase; // 0=attack, 1=decay, 2=sustain, 3=release
 
 		// Effects
-		byte effectMode;   // 0=none, 1=porta/arp
+		byte effectMode;   // 0=none, 1=vibrato, 2=arpeggio
 		bool portaUp;
 		bool portaDown;
 		int16 portaStep;
@@ -132,6 +132,16 @@ private:
 		byte arpeggioMask;
 		byte arpeggioPos;
 
+		// Vibrato
+		byte vibratoSpeed;  // Phase increment per tick
+		byte vibratoDepth;  // Amplitude in period units
+		int8 vibratoPos;    // Current phase position (oscillates)
+		int8 vibratoDir;    // +1 or -1
+
+		// Noise
+		bool noiseEnabled;  // Instrument flags bit 1: noise mode
+		bool freqSweep;     // Instrument flags bit 2: frequency sweep
+
 		// Period
 		int16 basePeriod;
 		int16 outputPeriod;
@@ -224,6 +234,13 @@ void EclipseAtariMusicStream::loadTables() {
 		_periods[i] = readDataWord(kTEPeriodTableOffset + i * 2);
 	}
 
+	// Fix note 46: corrupted by data artifact ($3095 instead of $010D).
+	// Correct value interpolated from surrounding notes (45=$011D, 47=$00FE).
+	if (_periods[46] == 0x3095) {
+		_periods[46] = 0x010D;
+		debug(3, "TE-Atari: Fixed corrupted period for note 46 ($3095 -> $010D)");
+	}
+
 	// Arpeggio interval table: 8 bytes at TEXT+$0CC8
 	for (int i = 0; i < 8; i++) {
 		_arpeggioIntervals[i] = readDataByte(kTEArpeggioIntervalsOffset + i);
@@ -347,14 +364,14 @@ void EclipseAtariMusicStream::readOrderList(int ch) {
 			continue;
 		}
 
-		if (cmd < kTEMaxPatterns && _patternPtrs[cmd] > 0 && _patternPtrs[cmd] < _dataSize) {
+		if (cmd < _numPatterns && _patternPtrs[cmd] > 0 && _patternPtrs[cmd] < _dataSize) {
 			c.patternOffset = _patternPtrs[cmd];
 			c.patternPos = 0;
 			debugC(3, kFreescapeDebugParser, "TE-Atari: ch%d order -> pattern %d (offset $%04X)", ch, cmd, c.patternOffset);
 		} else {
-			warning("TE-Atari: ch%d pattern index %d invalid", ch, cmd);
-			c.patternOffset = _patternPtrs[0];
-			c.patternPos = 0;
+			// Invalid pattern index — skip it and try next order entry
+			debugC(3, kFreescapeDebugParser, "TE-Atari: ch%d skipping invalid pattern index %d", ch, cmd);
+			continue;
 		}
 		return;
 	}
@@ -407,15 +424,7 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
 		}
 
 		if (cmd >= 0xC0) {
-			// Instrument select
-			// The TEMUSIC.ST encoding: (cmd & $1F) selects envelope/instrument
-			// The instrument index is encoded as (cmd & $1F) >> 3 for the
-			// instrument table. However, looking at the disassembly:
-			// AND $1F, ASL 3 → stored as 8*index in $3C(a0)
-			// Then at $0950: d0 = $3C(a0), lea $0D60, a2 += d0
-			// So the $3C field stores instrumentIdx * 8, and the table
-			// lookup uses it directly as byte offset.
-			// For our purposes: instrument index = (cmd & 0x1F)
+			// Instrument select: (cmd & $1F) = instrument index
 			byte instIdx = cmd & 0x1F;
 			if (instIdx < kTENumInstruments) {
 				c.instrumentIdx = instIdx;
@@ -425,11 +434,27 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
 				c.attackRate = inst.attackRate;
 				c.releaseRate = inst.releaseRate;
 
-				if (inst.arpeggioData != 0) {
-					c.effectMode = 1;
+				// Instrument effect type: high nibble = vibrato speed,
+				// low nibble = vibrato depth (in period units)
+				if (inst.effectType != 0) {
+					c.effectMode = 1; // vibrato
+					c.vibratoSpeed = (inst.effectType >> 4) & 0x0F;
+					c.vibratoDepth = inst.effectType & 0x0F;
+					c.vibratoPos = 0;
+					c.vibratoDir = 1;
+					c.portaUp = false;
+					c.portaDown = false;
+				} else if (inst.arpeggioData != 0) {
+					c.effectMode = 2; // arpeggio
 					c.arpeggioMask = inst.arpeggioData;
 					buildArpeggioTable(inst.arpeggioData);
+				} else {
+					c.effectMode = 0;
 				}
+
+				// Noise flags from instrument byte 7
+				c.noiseEnabled = (inst.flags & 0x02) != 0;
+				c.freqSweep = (inst.flags & 0x04) != 0;
 			}
 			continue;
 		}
@@ -456,19 +481,23 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
 		}
 
 		if (cmd == 0x7D) {
+			// Arpeggio / effect mode 1 — param is bitmask into interval table
 			byte param = readDataByte(c.patternOffset + c.patternPos);
 			c.patternPos++;
-			c.effectMode = 1;
+			c.effectMode = 2; // arpeggio
 			c.arpeggioMask = param;
+			c.arpeggioPos = 0;
 			buildArpeggioTable(param);
 			continue;
 		}
 
 		if (cmd == 0x7C) {
+			// Effect mode 2 — alternate arpeggio/vibrato
 			byte param = readDataByte(c.patternOffset + c.patternPos);
 			c.patternPos++;
-			c.effectMode = 1;
+			c.effectMode = 2; // arpeggio
 			c.arpeggioMask = param;
+			c.arpeggioPos = 0;
 			buildArpeggioTable(param);
 			continue;
 		}
@@ -550,17 +579,12 @@ void EclipseAtariMusicStream::triggerNote(int ch) {
 void EclipseAtariMusicStream::processEffects(int ch) {
 	ChannelState &c = _channels[ch];
 
-	if (c.effectMode == 0) {
-		c.outputPeriod = c.basePeriod;
-		return;
-	}
-
+	// Portamento takes priority (active during porta regardless of effectMode)
 	if (c.portaUp) {
 		c.basePeriod -= c.portaStep;
 		if (c.basePeriod <= c.portaTarget) {
 			c.basePeriod = c.portaTarget;
 			c.portaUp = false;
-			c.effectMode = 0;
 		}
 		c.outputPeriod = c.basePeriod;
 		return;
@@ -571,14 +595,30 @@ void EclipseAtariMusicStream::processEffects(int ch) {
 		if (c.basePeriod >= c.portaTarget) {
 			c.basePeriod = c.portaTarget;
 			c.portaDown = false;
-			c.effectMode = 0;
 		}
 		c.outputPeriod = c.basePeriod;
 		return;
 	}
 
-	// Arpeggio
-	if (_arpeggioTableLen > 0) {
+	if (c.effectMode == 1 && c.vibratoDepth > 0) {
+		// Vibrato: triangle wave pitch oscillation around basePeriod
+		c.vibratoPos += c.vibratoDir;
+		if (c.vibratoPos >= (int8)c.vibratoDepth) {
+			c.vibratoPos = c.vibratoDepth;
+			c.vibratoDir = -1;
+		} else if (c.vibratoPos <= -(int8)c.vibratoDepth) {
+			c.vibratoPos = -(int8)c.vibratoDepth;
+			c.vibratoDir = 1;
+		}
+		// Scale vibrato offset by speed (higher speed = faster oscillation)
+		int16 offset = c.vibratoPos * c.vibratoSpeed;
+		c.outputPeriod = c.basePeriod + offset;
+		if (c.outputPeriod < 1) c.outputPeriod = 1;
+		return;
+	}
+
+	if (c.effectMode == 2 && _arpeggioTableLen > 0) {
+		// Arpeggio: cycle through note offsets from interval table
 		int note = c.note + c.transpose;
 		int offset = _arpeggioTable[c.arpeggioPos % _arpeggioTableLen];
 		note += offset;
@@ -588,9 +628,10 @@ void EclipseAtariMusicStream::processEffects(int ch) {
 		c.arpeggioPos++;
 		if (c.arpeggioPos >= _arpeggioTableLen)
 			c.arpeggioPos = 0;
-	} else {
-		c.outputPeriod = c.basePeriod;
+		return;
 	}
+
+	c.outputPeriod = c.basePeriod;
 }
 
 // ---------------------------------------------------------------------------
@@ -664,7 +705,7 @@ void EclipseAtariMusicStream::buildArpeggioTable(byte mask) {
 // ---------------------------------------------------------------------------
 
 void EclipseAtariMusicStream::writeYMRegisters() {
-	byte mixer = 0x3F; // Start with all disabled
+	byte mixer = 0x3F; // Start with all disabled (bits 0-2=tone, bits 3-5=noise)
 
 	for (int ch = 0; ch < kTENumChannels; ch++) {
 		ChannelState &c = _channels[ch];
@@ -675,9 +716,18 @@ void EclipseAtariMusicStream::writeYMRegisters() {
 			continue;
 		}
 
-		// Enable tone for this channel
+		// Enable tone for this channel (bits 0-2)
 		mixer &= ~(1 << ch);
 
+		// Enable noise for this channel if instrument has noise flag (bits 3-5)
+		if (c.noiseEnabled) {
+			mixer &= ~(1 << (ch + 3));
+			// Set noise period from note (lower period = higher pitched noise)
+			byte noisePeriod = (c.outputPeriod >> 4) & 0x1F;
+			if (noisePeriod == 0) noisePeriod = 1;
+			setReg(6, noisePeriod);
+		}
+
 		// Set tone period (2 registers per channel)
 		uint16 period = (uint16)c.outputPeriod;
 		setReg(ch * 2, period & 0xFF);       // Fine tune


Commit: c267b8c31fb0209a8ba57def854571f9ceec5c56
    https://github.com/scummvm/scummvm/commit/c267b8c31fb0209a8ba57def854571f9ceec5c56
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00

Commit Message:
FREESCAPE: improve eclipse ui in atari

Changed paths:
    engines/freescape/font.cpp
    engines/freescape/font.h
    engines/freescape/freescape.h
    engines/freescape/games/eclipse/atari.cpp
    engines/freescape/games/eclipse/eclipse.cpp
    engines/freescape/games/eclipse/eclipse.h


diff --git a/engines/freescape/font.cpp b/engines/freescape/font.cpp
index d83ac3b5980..1014f01c9fb 100644
--- a/engines/freescape/font.cpp
+++ b/engines/freescape/font.cpp
@@ -59,6 +59,8 @@ Common::String centerAndPadString(const Common::String &str, int size) {
 Font::Font() {
 	_backgroundColor = 0;
 	_secondaryColor = 0;
+	_tertiaryColor = 0;
+	_quaternaryColor = 0;
 	_kerningOffset = 0;
 	_charWidth = 0;
 	_chars.clear();
@@ -68,6 +70,8 @@ Font::Font(Common::Array<Graphics::ManagedSurface *> &chars) {
 	_chars = chars;
 	_backgroundColor = 0;
 	_secondaryColor = 0;
+	_tertiaryColor = 0;
+	_quaternaryColor = 0;
 	_kerningOffset = 0;
 	_charWidth = 8;
 }
@@ -95,6 +99,14 @@ void Font::setSecondaryColor(uint32 color) {
 	_secondaryColor = color;
 }
 
+void Font::setTertiaryColor(uint32 color) {
+	_tertiaryColor = color;
+}
+
+void Font::setQuaternaryColor(uint32 color) {
+	_quaternaryColor = color;
+}
+
 void Font::setBackground(uint32 color) {
 	_backgroundColor = color;
 }
@@ -118,19 +130,25 @@ void Font::drawChar(Graphics::Surface *dst, uint32 chr, int x, int y, uint32 col
 	uint8 rb, gb, bb;
 	uint8 rp, gp, bp;
 	uint8 rs, gs, bs;
+	uint8 rt, gt, bt;
+	uint8 rq, gq, bq;
 
 	dst->format.colorToRGB(color, rp, gp, bp);
 	dst->format.colorToRGB(_secondaryColor, rs, gs, bs);
 	dst->format.colorToRGB(_backgroundColor, rb, gb, bb);
+	dst->format.colorToRGB(_tertiaryColor, rt, gt, bt);
+	dst->format.colorToRGB(_quaternaryColor, rq, gq, bq);
 
-	byte palette[3][3] = {
+	byte palette[5][3] = {
 		{ rb, gb, bb },
 		{ rp, gp, bp },
 		{ rs, gs, bs },
+		{ rt, gt, bt },
+		{ rq, gq, bq },
 	};
 
 	if (surface.format != dst->format)
-		surface.convertToInPlace(dst->format, (byte *)palette, 3);
+		surface.convertToInPlace(dst->format, (byte *)palette, 5);
 
 	if (_backgroundColor == dst->format.ARGBToColor(0x00, 0x00, 0x00, 0x00))
 		dst->copyRectToSurfaceWithKey(surface, x, y, Common::Rect(0, 0, MIN(int(surface.w), _charWidth), surface.h), dst->format.ARGBToColor(0xFF, 0x00, 0x00, 0x00));
@@ -214,6 +232,40 @@ Common::Array<Graphics::ManagedSurface *> FreescapeEngine::getCharsAmigaAtariInt
 	return chars;
 }
 
+Common::Array<Graphics::ManagedSurface *> FreescapeEngine::getChars4Plane(Common::SeekableReadStream *file, int offset, int charsNumber) {
+	// 4-bitplane font: each glyph = 8 rows x 4 planes x 2 bytes = 64 bytes
+	// Used by Eclipse Atari ST for the bordered/embossed UI font
+	int glyphSize = 64;
+	int fontSize = glyphSize * charsNumber;
+	byte *fontBuffer = (byte *)malloc(fontSize);
+	file->seek(offset);
+	file->read(fontBuffer, fontSize);
+
+	Common::Array<Graphics::ManagedSurface *> chars;
+	for (int c = 0; c < charsNumber - 1; c++) {
+		Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+		surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
+		for (int row = 0; row < 8; row++) {
+			int rowOff = c * glyphSize + row * 8;
+			uint16 p0 = READ_BE_UINT16(&fontBuffer[rowOff + 0]);
+			uint16 p1 = READ_BE_UINT16(&fontBuffer[rowOff + 2]);
+			uint16 p2 = READ_BE_UINT16(&fontBuffer[rowOff + 4]);
+			uint16 p3 = READ_BE_UINT16(&fontBuffer[rowOff + 6]);
+			for (int col = 0; col < 8; col++) {
+				int bit = 15 - col; // MSB = leftmost pixel
+				byte color = ((p0 >> bit) & 1)
+				           | (((p1 >> bit) & 1) << 1)
+				           | (((p2 >> bit) & 1) << 2)
+				           | (((p3 >> bit) & 1) << 3);
+				surface->setPixel(col, row, color);
+			}
+		}
+		chars.push_back(surface);
+	}
+	free(fontBuffer);
+	return chars;
+}
+
 Common::Array<Graphics::ManagedSurface *> FreescapeEngine::getCharsAmigaAtari(Common::SeekableReadStream *file, int offset, int charsNumber) {
 	return getCharsAmigaAtariInternal(8, 8, isEclipse() ? 0 : 1, isDriller() ? 33 : 16, isDriller() ? 32 : 16, file, offset, charsNumber);
 }
diff --git a/engines/freescape/font.h b/engines/freescape/font.h
index 80ca9acbf94..65f5cc69113 100644
--- a/engines/freescape/font.h
+++ b/engines/freescape/font.h
@@ -37,6 +37,8 @@ public:
 
 	void setBackground(uint32 color);
 	void setSecondaryColor(uint32 color);
+	void setTertiaryColor(uint32 color);
+	void setQuaternaryColor(uint32 color);
 	int getFontHeight() const override;
 	int getMaxCharWidth() const override;
 	int getCharWidth(uint32 chr) const override;
@@ -51,6 +53,8 @@ private:
 	Common::Array<Graphics::ManagedSurface *> _chars;
 	uint32 _backgroundColor;
 	uint32 _secondaryColor;
+	uint32 _tertiaryColor;
+	uint32 _quaternaryColor;
 	int _kerningOffset;
 	int _charWidth;
 };
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 6a16947929d..0cd8bdeb912 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -107,6 +107,8 @@ enum FreescapeAction {
 	kActionSelectPrince,
 	kActionSelectPrincess,
 	kActionQuit,
+	kActionToggleFlashlight,
+
 	// Demo actions
 	kActionUnknownKey,
 	kActionWait
@@ -574,6 +576,7 @@ public:
 	Common::Array<Graphics::ManagedSurface *> getChars(Common::SeekableReadStream *file, int offset, int charsNumber);
 	Common::Array<Graphics::ManagedSurface *> getCharsAmigaAtariInternal(int sizeX, int sizeY, int additional, int m1, int m2, Common::SeekableReadStream *file, int offset, int charsNumber);
 	Common::Array<Graphics::ManagedSurface *> getCharsAmigaAtari(Common::SeekableReadStream *file, int offset, int charsNumber);
+	Common::Array<Graphics::ManagedSurface *> getChars4Plane(Common::SeekableReadStream *file, int offset, int charsNumber);
 	Common::StringArray _currentAreaMessages;
 	Common::StringArray _currentEphymeralMessages;
 	Font _font;
diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 1ad82118ed6..af96e2b0aea 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -158,13 +158,220 @@ void EclipseEngine::drawCPCUI(Graphics::Surface *surface) {
 	drawEclipseIndicator(surface, 228, 0, front, other);
 }*/
 
+// Border palette from CONSOLE.NEO (Atari ST 9-bit $0RGB, scaled to 8-bit).
+// Used for font rendering and sprite conversion.
+static const byte kBorderPalette[16 * 3] = {
+	0, 0, 0,        // 0: $000 black
+	145, 72, 0,     // 1: $420 dark brown
+	182, 109, 36,   // 2: $531 medium brown
+	218, 145, 36,   // 3: $641 golden brown
+	255, 182, 36,   // 4: $751 bright gold
+	255, 218, 145,  // 5: $764
+	218, 218, 218,  // 6: $666
+	182, 182, 182,  // 7: $555
+	145, 145, 145,  // 8: $444
+	109, 109, 109,  // 9: $333
+	72, 72, 72,     // 10: $222
+	182, 36, 0,     // 11: $510 dark red
+	255, 72, 0,     // 12: $720
+	255, 109, 0,    // 13: $730
+	255, 145, 0,    // 14: $740
+	255, 255, 255,  // 15: $777 white
+};
+
+// Load raw 4-plane pixel data (no mask) from stream into a CLUT8 surface.
+static Graphics::ManagedSurface *loadAtariSTRawSprite(Common::SeekableReadStream *stream,
+		int pixelOffset, int cols, int rows) {
+	stream->seek(pixelOffset);
+	Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+	surface->create(cols * 16, rows, Graphics::PixelFormat::createFormatCLUT8());
+	for (int row = 0; row < rows; row++) {
+		for (int col = 0; col < cols; col++) {
+			uint16 p0 = stream->readUint16BE();
+			uint16 p1 = stream->readUint16BE();
+			uint16 p2 = stream->readUint16BE();
+			uint16 p3 = stream->readUint16BE();
+			for (int bit = 15; bit >= 0; bit--) {
+				int x = col * 16 + (15 - bit);
+				byte color = ((p0 >> bit) & 1)
+				           | (((p1 >> bit) & 1) << 1)
+				           | (((p2 >> bit) & 1) << 2)
+				           | (((p3 >> bit) & 1) << 3);
+				surface->setPixel(x, row, color);
+			}
+		}
+	}
+	return surface;
+}
+
+static Graphics::ManagedSurface *loadAtariSTSprite(Common::SeekableReadStream *stream,
+		int maskOffset, int pixelOffset, int cols, int rows) {
+	// Read per-column mask (1 word per column, same for all rows)
+	stream->seek(maskOffset);
+	Common::Array<uint16> mask(cols);
+	for (int c = 0; c < cols; c++)
+		mask[c] = stream->readUint16BE();
+
+	// Read pixel data: sequential 4-plane words, cols per row
+	stream->seek(pixelOffset);
+	Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+	surface->create(cols * 16, rows, Graphics::PixelFormat::createFormatCLUT8());
+	for (int row = 0; row < rows; row++) {
+		for (int col = 0; col < cols; col++) {
+			uint16 p0 = stream->readUint16BE();
+			uint16 p1 = stream->readUint16BE();
+			uint16 p2 = stream->readUint16BE();
+			uint16 p3 = stream->readUint16BE();
+			for (int bit = 15; bit >= 0; bit--) {
+				int x = col * 16 + (15 - bit);
+				byte color = ((p0 >> bit) & 1)
+				           | (((p1 >> bit) & 1) << 1)
+				           | (((p2 >> bit) & 1) << 2)
+				           | (((p3 >> bit) & 1) << 3);
+				surface->setPixel(x, row, color);
+			}
+		}
+	}
+	return surface;
+}
+
+void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
+	// Border palette colors for the 4-plane font (from CONSOLE.NEO).
+	// The Atari ST uses raster interrupts to switch palettes between
+	// the 3D viewport (area palette) and the border/UI area (border palette).
+	uint32 pal[5];
+	pal[0] = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0);
+	pal[1] = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 145, 72, 0);
+	pal[2] = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 182, 109, 36);
+	pal[3] = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 218, 145, 36);
+	pal[4] = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 255, 182, 36);
+	_font.setBackground(pal[0]);
+	_font.setSecondaryColor(pal[2]);
+	_font.setTertiaryColor(pal[3]);
+	_font.setQuaternaryColor(pal[4]);
+
+	// Score: Font B at x=$8B(139), y=$06(6) — from $11A66/$11A6E
+	int score = _gameStateVars[k8bitVariableScore];
+	drawScoreString(score, 139, 6, pal[1], pal[0], surface);
+
+	// Room name / messages: $CE2 at x=$55(85), y=$77(119) — from $11BDC-$11C02
+	Common::String message;
+	int deadline;
+	getLatestMessages(message, deadline);
+	if (deadline <= _countdown) {
+		drawStringInSurface(message, 85, 119, pal[1], pal[2], pal[0], surface);
+		_temporaryMessages.push_back(message);
+		_temporaryMessageDeadlines.push_back(deadline);
+	} else if (!_currentAreaMessages.empty())
+		drawStringInSurface(_currentArea->_name, 85, 119, pal[1], pal[2], pal[0], surface);
+
+	// Step indicator: $CDC at x=$4C(76), y=$77(119)
+	// d7 = $42 + (2 - _playerStepIndex) * 2, drawChar chr = d7 + 31
+	{
+		int d7 = 0x42 + (2 - _playerStepIndex) * 2;
+		int chr = d7 + 31;
+		_font.drawChar(surface, chr, 76, 119, pal[1]);
+	}
+
+	// Height indicator: $CDC at x=$E0(224), y=$77(119)
+	// d7 = $48 + (2 - _playerHeightNumber) * 2, drawChar chr = d7 + 31
+	{
+		int d7 = 0x48 + (2 - _playerHeightNumber) * 2;
+		int chr = d7 + 31;
+		_font.drawChar(surface, chr, 224, 119, pal[1]);
+	}
+
+	// Rotation/shooting indicator: $CDC at x=$E9(233), y=$77(119)
+	// d7 = $4E normally, $50 when shooting
+	if (_shootingFrames > 0) {
+		int chr = 0x50 + 31;
+		_font.drawChar(surface, chr, 233, 119, pal[1]);
+	} else {
+		int chr = 0x4E + 31;
+		_font.drawChar(surface, chr, 233, 119, pal[1]);
+	}
+
+	// Eclipse animation: sprite blit at x=$A0(160), y=$86(134) — from $1E9E/$1EA6
+	if (_eclipseSprites.size() >= 2) {
+		// Toggle between 2 frames based on countdown
+		int frame = (_countdown / 30) % 2;
+		surface->copyRectToSurface(*_eclipseSprites[frame], 160, 134,
+			Common::Rect(_eclipseSprites[frame]->w, _eclipseSprites[frame]->h));
+	}
+
+	// Shield energy bar: sprite blit at x=$80(128), y=$84(132) — from $11CD8/$11CE0
+	// 16 frames selected by shield level (0-15)
+	if (_shieldSprites.size() >= 16) {
+		int shieldLevel = _gameStateVars[k8bitVariableShield] * 15 / _maxShield;
+		shieldLevel = CLIP(shieldLevel, 0, 15);
+		surface->copyRectToSurface(*_shieldSprites[shieldLevel], 128, 132,
+			Common::Rect(_shieldSprites[shieldLevel]->w, _shieldSprites[shieldLevel]->h));
+	}
+
+	// Ankh indicators at y=$B6(182), x = (ankh_idx-1)*16 + 3 — from $11D88
+	drawIndicator(surface, 3, 182, 16);
+
+	// Compass at x=$30(48), y=$8B(139) — pre-rendered background + needle
+	if (_compassSprites.size() >= 37) {
+		// Map yaw (0-359) to lookup table index (0-71, 5 degrees each)
+		int lookupIdx = ((int)_yaw % 360) / 5;
+		if (lookupIdx < 0)
+			lookupIdx += 72;
+		int needleFrame = _compassLookup[lookupIdx];
+		if (needleFrame < (int)_compassSprites.size()) {
+			surface->copyRectToSurface(*_compassSprites[needleFrame], 48, 139,
+				Common::Rect(_compassSprites[needleFrame]->w, _compassSprites[needleFrame]->h));
+		}
+	}
+
+	// Lantern switch at x=$30(48), y=$91(145) — 2 frames (32x23), toggled with 'T' key
+	// Frame 0 = on, frame 1 = off
+	if (_lanternSwitchSprites.size() >= 2) {
+		int switchFrame = _flashlightOn ? 0 : 1;
+		surface->copyRectToSurface(*_lanternSwitchSprites[switchFrame], 48, 145,
+			Common::Rect(_lanternSwitchSprites[switchFrame]->w, _lanternSwitchSprites[switchFrame]->h));
+	}
+
+	// Lantern light animation overlay at (48, 139) — 6 frames, 32x6, toggled with 'T' key
+	if (_flashlightOn && _lanternLightSprites.size() >= 6) {
+		int lightFrame = (_ticks / 8) % 6;
+		surface->copyRectToSurface(*_lanternLightSprites[lightFrame], 48, 139,
+			Common::Rect(_lanternLightSprites[lightFrame]->w, _lanternLightSprites[lightFrame]->h));
+	}
+
+	// Shooting crosshair overlay at x=$80(128), y=$9F(159)
+	if (_shootingFrames > 0 && _shootSprites.size() >= 2) {
+		int shootFrame = (_shootingFrames > 5) ? 1 : 0;
+		surface->copyRectToSurface(*_shootSprites[shootFrame], 128, 159,
+			Common::Rect(_shootSprites[shootFrame]->w, _shootSprites[shootFrame]->h));
+	}
+
+	// Analog clock — kept from existing implementation
+	uint8 r, g, b;
+	uint32 color = _currentArea->_underFireBackgroundColor;
+	_gfx->readFromPalette(color, r, g, b);
+	uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+
+	color = _currentArea->_usualBackgroundColor;
+	if (_gfx->_colorRemaps && _gfx->_colorRemaps->contains(color))
+		color = (*_gfx->_colorRemaps)[color];
+	_gfx->readFromPalette(color, r, g, b);
+	uint32 back = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+
+	color = _currentArea->_inkColor;
+	_gfx->readFromPalette(color, r, g, b);
+	uint32 other = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
+
+	drawAnalogClock(surface, 90, 172, back, other, front);
+}
+
 void EclipseEngine::loadAssetsAtariFullGame() {
 	Common::File file;
 	file.open("0.tec");
 	_title = loadAndConvertNeoImage(&file, 0x17ac);
 	file.close();
 
-    Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.tec", "0.tec", 0x1774 - 4 * 1024);
+	Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.tec", "0.tec", 0x1774 - 4 * 1024);
 	parseAmigaAtariHeader(stream);
 
 	loadMessagesVariableSize(stream, 0x87a6, 28);
@@ -183,17 +390,121 @@ void EclipseEngine::loadAssetsAtariFullGame() {
 	stream->read(_musicData.data(), kTEMusicTextSize);
 	debug(3, "TE-Atari: Loaded TEMUSIC.ST TEXT segment (%d bytes)", kTEMusicTextSize);
 
-	/*
-	loadFonts(stream, 0xd06b, _fontBig);
-	loadFonts(stream, 0xd49a, _fontMedium);
-	loadFonts(stream, 0xd49b, _fontSmall);
+	// UI font (Font A): 4-plane 16-color bordered font at prog $24C3E (file offset $24C5A)
+	// 85 characters: ASCII text (32-116) plus special indicator glyphs (65-84)
+	Common::Array<Graphics::ManagedSurface *> chars;
+	chars = getChars4Plane(stream, 0x24C5A, 85);
+	_font = Font(chars);
+
+	// Score font (Font B): 4-plane 10-glyph font at prog $249BE (file offset $249DA)
+	// Dedicated score digits 0-9 with different bordered style
+	Common::Array<Graphics::ManagedSurface *> scoreChars;
+	scoreChars = getChars4Plane(stream, 0x249DA, 11);
+	_fontScore = Font(scoreChars);
+
+	// All sprite addresses below are program addresses from the 68K disassembly.
+	// The decrypted stream includes a $1C-byte GEMDOS header, so add $1C to
+	// convert program addresses to stream offsets.
+	static const int kHdr = 0x1C;
+
+	// Eclipse animation sprites: 2 frames, 16x13 pixels
+	// Descriptor at prog $1D2B8 (1 col × 13 rows), mask at +6, pixels at +8
+	// Frame 1 pixels at prog $1D7AB
+	_eclipseSprites.resize(2);
+	_eclipseSprites[0] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + kHdr, 1, 13);
+	_eclipseSprites[1] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D7AB + kHdr, 1, 13);
+
+	// Shield energy bar sprites: 16 frames, 16x16 pixels
+	// Descriptor at prog $1DA90 (1 col × 16 rows), mask at +6, pixels at +8
+	// Each frame = 128 bytes (16 rows × 4 words × 2 bytes)
+	_shieldSprites.resize(16);
+	for (int i = 0; i < 16; i++)
+		_shieldSprites[i] = loadAtariSTSprite(stream, 0x1DA96 + kHdr, 0x1DA98 + kHdr + i * 128, 1, 16);
+
+	// Ankh indicator: descriptor at prog $1B72C (1 col × 15 rows = 16x15), mask at +6, pixels at +8
+	Graphics::ManagedSurface *ankhManaged = loadAtariSTSprite(stream, 0x1B732 + kHdr, 0x1B734 + kHdr, 1, 15);
+	ankhManaged->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
+	Graphics::Surface *ankhSurface = new Graphics::Surface();
+	ankhSurface->copyFrom(*ankhManaged);
+	delete ankhManaged;
+	_indicators.push_back(ankhSurface);
+
+	// Compass background at prog $20986 (32x27, raw 4-plane) and needle at prog $20B36
+	// (37 frames, 32x27 each, stride 432 bytes). Pre-composite background + needle.
+	{
+		Graphics::ManagedSurface *compassBG = loadAtariSTRawSprite(stream, 0x20986 + kHdr, 2, 27);
+
+		// Load compass direction lookup table (72 entries at prog $1542)
+		stream->seek(0x1542 + kHdr);
+		stream->read(_compassLookup, 72);
+
+		// Find max needle frame index
+		int maxFrame = 0;
+		for (int i = 0; i < 72; i++)
+			if (_compassLookup[i] < 200 && _compassLookup[i] > maxFrame)
+				maxFrame = _compassLookup[i];
+
+		int numFrames = maxFrame + 1;
+		_compassSprites.resize(numFrames);
+		for (int f = 0; f < numFrames; f++) {
+			// Load needle frame (raw 4-plane, no mask)
+			Graphics::ManagedSurface *needle = loadAtariSTRawSprite(stream,
+				0x20B36 + kHdr + f * 432, 2, 27);
+
+			// Composite: copy background, then overlay needle where non-zero
+			Graphics::ManagedSurface *composite = new Graphics::ManagedSurface();
+			composite->create(32, 27, Graphics::PixelFormat::createFormatCLUT8());
+			composite->copyFrom(*compassBG);
+			for (int y = 0; y < 27; y++) {
+				for (int x = 0; x < 32; x++) {
+					byte needlePixel = *(const byte *)needle->getBasePtr(x, y);
+					if (needlePixel != 0)
+						composite->setPixel(x, y, needlePixel);
+				}
+			}
+			delete needle;
+
+			// Convert to target format
+			composite->convertToInPlace(_gfx->_texturePixelFormat,
+				const_cast<byte *>(kBorderPalette), 16);
+			_compassSprites[f] = composite;
+		}
+		delete compassBG;
+	}
 
-	load8bitBinary(stream, 0x20918, 16);
-	loadMessagesVariableSize(stream, 0x3f6f, 66);
+	// Lantern light animation: 6 frames, 32x6, at prog $2026A, stride 96 bytes
+	_lanternLightSprites.resize(6);
+	for (int i = 0; i < 6; i++) {
+		_lanternLightSprites[i] = loadAtariSTRawSprite(stream, 0x2026A + kHdr + i * 96, 2, 6);
+		_lanternLightSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+			const_cast<byte *>(kBorderPalette), 16);
+	}
 
-	loadPalettes(stream, 0x204d6);
-	loadGlobalObjects(stream, 0x32f6, 24);
-	loadSoundsFx(stream, 0x266e8, 11);*/
+	// Lantern switch: 2 frames, 32x23, at prog $204B4, stride $170 bytes
+	// Frame 0 = on, frame 1 = off (toggled with 'T' key)
+	_lanternSwitchSprites.resize(2);
+	_lanternSwitchSprites[0] = loadAtariSTRawSprite(stream, 0x204B4 + kHdr, 2, 23);
+	_lanternSwitchSprites[1] = loadAtariSTRawSprite(stream, 0x204B4 + 0x170 + kHdr, 2, 23);
+	for (auto &sprite : _lanternSwitchSprites)
+		sprite->convertToInPlace(_gfx->_texturePixelFormat,
+			const_cast<byte *>(kBorderPalette), 16);
+
+	// Shooting crosshair sprites: 2 frames with mask, at prog $1CC26 and $1CDC0
+	// Frame 0: 32x25 (2 cols), frame 1: 48x25 (3 cols)
+	_shootSprites.resize(2);
+	_shootSprites[0] = loadAtariSTSprite(stream, 0x1CC2C + kHdr, 0x1CC30 + kHdr, 2, 25);
+	_shootSprites[1] = loadAtariSTSprite(stream, 0x1CDC6 + kHdr, 0x1CDCC + kHdr, 3, 25);
+	for (auto &sprite : _shootSprites)
+		sprite->convertToInPlace(_gfx->_texturePixelFormat,
+			const_cast<byte *>(kBorderPalette), 16);
+
+	// Convert eclipse and shield sprites from CLUT8 to target format using border palette
+	for (auto &sprite : _eclipseSprites)
+		sprite->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
+	for (auto &sprite : _shieldSprites)
+		sprite->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
+
+	_fontLoaded = true;
 }
 
 } // End of namespace Freescape
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index f052d25dfe7..6b5d0e5ef5d 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -94,6 +94,7 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
 	_lastFiveSeconds = 0;
 	_lastSecond = -1;
 	_resting = false;
+	_flashlightOn = false;
 }
 
 void EclipseEngine::initGameState() {
@@ -109,6 +110,7 @@ void EclipseEngine::initGameState() {
 	_lastThirtySeconds = seconds / 30;
 	_lastFiveSeconds = seconds / 5;
 	_resting = false;
+	_flashlightOn = false;
 
 	// Start playing music, if any, in any supported format
 	playMusic("Total Eclipse Theme");
@@ -292,6 +294,11 @@ void EclipseEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *in
 	act->setCustomEngineActionEvent(kActionFaceForward);
 	act->addDefaultInputMapping("f");
 	engineKeyMap->addAction(act);
+
+	act = new Common::Action("FLASHLIGHT", _("Toggle Flashlight"));
+	act->setCustomEngineActionEvent(kActionToggleFlashlight);
+	act->addDefaultInputMapping("t");
+	engineKeyMap->addAction(act);
 }
 
 void EclipseEngine::gotoArea(uint16 areaID, int entranceID) {
@@ -531,6 +538,8 @@ void EclipseEngine::pressedKey(const int keycode) {
 	} else if (keycode == kActionFaceForward) {
 		_pitch = 0;
 		updateCamera();
+	} else if (keycode == kActionToggleFlashlight) {
+		_flashlightOn = !_flashlightOn;
 	}
 }
 
@@ -770,19 +779,46 @@ void EclipseEngine::drawScoreString(int score, int x, int y, uint32 front, uint3
 			drawStringInSurface(scoreStr, x, y, front, back, surface);
 			return;
 		}
+	}
 
+	// Atari ST: use Font B (_fontScore) with dedicated score digit glyphs.
+	// Font B has 10 glyphs (0-9) for digits. In the original, the score bytes
+	// have $2F subtracted to map '0'→glyph 0, '1'→glyph 1, etc.
+	// For drawChar: chr = glyph_index + 32, so digit '0' → chr 32, '9' → chr 41.
+	if (isAtariST()) {
+		_fontScore.setBackground(back);
+		_fontScore.setSecondaryColor(front);
+		// Font B uses palette indices 1-4 like Font A
+		uint32 pal2 = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 182, 109, 36);
+		uint32 pal3 = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 218, 145, 36);
+		uint32 pal4 = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 255, 182, 36);
+		_fontScore.setSecondaryColor(pal2);
+		_fontScore.setTertiaryColor(pal3);
+		_fontScore.setQuaternaryColor(pal4);
+		for (int i = 0; i < int(scoreStr.size()); i++) {
+			int chr = (scoreStr[i] - '0') + 32;
+			_fontScore.drawChar(surface, chr, x, y, front);
+			x += 8;
+		}
+		return;
 	}
 
 	// Start in x,y and draw each digit, from left to right, adding a gap every 3 digits
 	int gapSize = isC64() ? 8 : 4;
+	int charStep = 8;
+
+	Font *scoreFont = &_font;
+	scoreFont->setBackground(back);
+	scoreFont->setSecondaryColor(front);
 
 	for (int i = 0; i < int(scoreStr.size()); i++) {
-		drawStringInSurface(Common::String(scoreStr[i]), x, y, front, back, surface);
-		x += 8;
+		Common::String digit(scoreStr[i]);
+		digit.toUppercase();
+		scoreFont->drawString(surface, digit, x, y, _screenW, front);
+		x += charStep;
 		if ((i - scoreStr.size() + 1) % 3 == 1)
 			x += gapSize;
 	}
-
 }
 
 
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index 5f8f942523d..c6589b090c2 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -59,6 +59,7 @@ public:
 	int _soundIndexEndFalling;
 
 	bool _resting;
+	bool _flashlightOn;
 	int _lastThirtySeconds;
 	int _lastFiveSeconds;
 
@@ -87,6 +88,7 @@ public:
 	void drawCPCUI(Graphics::Surface *surface) override;
 	void drawC64UI(Graphics::Surface *surface) override;
 	void drawZXUI(Graphics::Surface *surface) override;
+	void drawAmigaAtariSTUI(Graphics::Surface *surface) override;
 	void drawAnalogClock(Graphics::Surface *surface, int x, int y, uint32 colorHand1, uint32 colorHand2, uint32 colorBack);
 	void drawAnalogClockHand(Graphics::Surface *surface, int x, int y, double degrees, double magnitude, uint32 color);
 	void drawCompass(Graphics::Surface *surface, int x, int y, double degrees, double magnitude, uint32 color);
@@ -98,6 +100,16 @@ public:
 
 	Common::Array<byte> _musicData; // TEMUSIC.ST TEXT segment (Atari ST)
 
+	// Atari ST UI sprites (extracted from binary, pre-converted to target format)
+	Font _fontScore; // Font B (10 score digit glyphs, 4-plane at $249BE)
+	Common::Array<Graphics::ManagedSurface *> _eclipseSprites; // 2 eclipse animation frames (16x13)
+	Common::Array<Graphics::ManagedSurface *> _shieldSprites;  // 16 shield level frames (16x16)
+	Common::Array<Graphics::ManagedSurface *> _compassSprites; // 37 pre-composited compass frames (32x27)
+	Common::Array<Graphics::ManagedSurface *> _lanternLightSprites;  // 6 lantern light animation frames (32x6)
+	Common::Array<Graphics::ManagedSurface *> _lanternSwitchSprites; // 2 lantern on/off frames (32x23)
+	Common::Array<Graphics::ManagedSurface *> _shootSprites;         // 2 shooting crosshair frames (32x25, 48x25)
+	byte _compassLookup[72];  // direction-to-needle-frame lookup table
+
 	bool checkIfGameEnded() override;
 	void endGame() override;
 	void loadSoundsFx(Common::SeekableReadStream *file, int offset, int number) override;


Commit: 8be72a7c1e9c2de86208d7c7d40f84d5ce2ab83a
    https://github.com/scummvm/scummvm/commit/8be72a7c1e9c2de86208d7c7d40f84d5ce2ab83a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00

Commit Message:
FREESCAPE: improved eclipse ui in atari

Changed paths:
    engines/freescape/games/eclipse/atari.cpp


diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index af96e2b0aea..b8016af15a1 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -311,25 +311,22 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 	// Ankh indicators at y=$B6(182), x = (ankh_idx-1)*16 + 3 — from $11D88
 	drawIndicator(surface, 3, 182, 16);
 
-	// Compass at x=$30(48), y=$8B(139) — pre-rendered background + needle
+	// Compass at x=$B0(176), y=$97(151) — from sprite header at prog $2097C/$2097E
 	if (_compassSprites.size() >= 37) {
-		// Map yaw (0-359) to lookup table index (0-71, 5 degrees each)
-		int lookupIdx = ((int)_yaw % 360) / 5;
-		if (lookupIdx < 0)
-			lookupIdx += 72;
+		// Normalize yaw to 0-359 first (C++ modulo can return negative), then map to table index
+		int deg = ((int)_yaw % 360 + 360) % 360;
+		int lookupIdx = deg / 5;
 		int needleFrame = _compassLookup[lookupIdx];
 		if (needleFrame < (int)_compassSprites.size()) {
-			surface->copyRectToSurface(*_compassSprites[needleFrame], 48, 139,
+			surface->copyRectToSurface(*_compassSprites[needleFrame], 176, 151,
 				Common::Rect(_compassSprites[needleFrame]->w, _compassSprites[needleFrame]->h));
 		}
 	}
 
-	// Lantern switch at x=$30(48), y=$91(145) — 2 frames (32x23), toggled with 'T' key
-	// Frame 0 = on, frame 1 = off
-	if (_lanternSwitchSprites.size() >= 2) {
-		int switchFrame = _flashlightOn ? 0 : 1;
-		surface->copyRectToSurface(*_lanternSwitchSprites[switchFrame], 48, 145,
-			Common::Rect(_lanternSwitchSprites[switchFrame]->w, _lanternSwitchSprites[switchFrame]->h));
+	// Lantern switch at x=$30(48), y=$91(145) — only drawn when lantern is ON
+	if (_flashlightOn && _lanternSwitchSprites.size() >= 2) {
+		surface->copyRectToSurface(*_lanternSwitchSprites[0], 48, 145,
+			Common::Rect(_lanternSwitchSprites[0]->w, _lanternSwitchSprites[0]->h));
 	}
 
 	// Lantern light animation overlay at (48, 139) — 6 frames, 32x6, toggled with 'T' key


Commit: 0f68a39da5551ee2ab39249cf79876b4c6c1f7db
    https://github.com/scummvm/scummvm/commit/0f68a39da5551ee2ab39249cf79876b4c6c1f7db
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00

Commit Message:
FREESCAPE: improved eclipse ui in atari

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


diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index b8016af15a1..4041921beba 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -291,16 +291,16 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 		_font.drawChar(surface, chr, 233, 119, pal[1]);
 	}
 
-	// Eclipse animation: sprite blit at x=$A0(160), y=$86(134) — from $1E9E/$1EA6
+	// Heart indicator: sprite blit at x=$A0(160), y=$86(134) — from $1E9E/$1EA6
+	// 2 frames: 0 = heart visible, 1 = heart hidden/dimmed. Blink cycle.
 	if (_eclipseSprites.size() >= 2) {
-		// Toggle between 2 frames based on countdown
-		int frame = (_countdown / 30) % 2;
+		int frame = (_ticks / 30) % 2;
 		surface->copyRectToSurface(*_eclipseSprites[frame], 160, 134,
 			Common::Rect(_eclipseSprites[frame]->w, _eclipseSprites[frame]->h));
 	}
 
-	// Shield energy bar: sprite blit at x=$80(128), y=$84(132) — from $11CD8/$11CE0
-	// 16 frames selected by shield level (0-15)
+	// Shield energy jar: sprite blit at x=$80(128), y=$84(132) — from $11CD8/$11CE0
+	// 16 frames showing jar fill level (0-15)
 	if (_shieldSprites.size() >= 16) {
 		int shieldLevel = _gameStateVars[k8bitVariableShield] * 15 / _maxShield;
 		shieldLevel = CLIP(shieldLevel, 0, 15);
@@ -308,8 +308,23 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 			Common::Rect(_shieldSprites[shieldLevel]->w, _shieldSprites[shieldLevel]->h));
 	}
 
-	// Ankh indicators at y=$B6(182), x = (ankh_idx-1)*16 + 3 — from $11D88
-	drawIndicator(surface, 3, 182, 16);
+	// Ankh indicators at y=$B6(182), x = i*16 + 3 — from $11D88
+	// Draw collected ankhs with transparency (skip black/color-0 pixels)
+	if (_ankhSprites.size() >= 5) {
+		uint32 transparentColor = pal[0]; // black
+		Graphics::ManagedSurface *ankh = _ankhSprites[3]; // frame 3 = fully visible
+		for (int i = 0; i < _gameStateVars[kVariableEclipseAnkhs] && i < 5; i++) {
+			int destX = 3 + 16 * i;
+			int destY = 182;
+			for (int y = 0; y < ankh->h; y++) {
+				for (int x = 0; x < ankh->w; x++) {
+					uint32 pixel = ankh->getPixel(x, y);
+					if (pixel != transparentColor)
+						surface->setPixel(destX + x, destY + y, pixel);
+				}
+			}
+		}
+	}
 
 	// Compass at x=$B0(176), y=$97(151) — from sprite header at prog $2097C/$2097E
 	if (_compassSprites.size() >= 37) {
@@ -404,12 +419,12 @@ void EclipseEngine::loadAssetsAtariFullGame() {
 	// convert program addresses to stream offsets.
 	static const int kHdr = 0x1C;
 
-	// Eclipse animation sprites: 2 frames, 16x13 pixels
+	// Heart indicator sprites: 2 frames, 16x13 pixels
 	// Descriptor at prog $1D2B8 (1 col × 13 rows), mask at +6, pixels at +8
-	// Frame 1 pixels at prog $1D7AB
+	// Frame 0 = heart visible, Frame 1 = heart hidden/dimmed
 	_eclipseSprites.resize(2);
 	_eclipseSprites[0] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + kHdr, 1, 13);
-	_eclipseSprites[1] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D7AB + kHdr, 1, 13);
+	_eclipseSprites[1] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + 104 + kHdr, 1, 13);
 
 	// Shield energy bar sprites: 16 frames, 16x16 pixels
 	// Descriptor at prog $1DA90 (1 col × 16 rows), mask at +6, pixels at +8
@@ -418,13 +433,14 @@ void EclipseEngine::loadAssetsAtariFullGame() {
 	for (int i = 0; i < 16; i++)
 		_shieldSprites[i] = loadAtariSTSprite(stream, 0x1DA96 + kHdr, 0x1DA98 + kHdr + i * 128, 1, 16);
 
-	// Ankh indicator: descriptor at prog $1B72C (1 col × 15 rows = 16x15), mask at +6, pixels at +8
-	Graphics::ManagedSurface *ankhManaged = loadAtariSTSprite(stream, 0x1B732 + kHdr, 0x1B734 + kHdr, 1, 15);
-	ankhManaged->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
-	Graphics::Surface *ankhSurface = new Graphics::Surface();
-	ankhSurface->copyFrom(*ankhManaged);
-	delete ankhManaged;
-	_indicators.push_back(ankhSurface);
+	// Ankh indicator: 5 fade-in frames at prog $1B734, 16x15 (1 col, stride 120 bytes)
+	// Mask at prog $1B732. Frame 3 = fully visible ankh.
+	_ankhSprites.resize(5);
+	for (int i = 0; i < 5; i++) {
+		_ankhSprites[i] = loadAtariSTSprite(stream, 0x1B732 + kHdr, 0x1B734 + kHdr + i * 120, 1, 15);
+		_ankhSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+			const_cast<byte *>(kBorderPalette), 16);
+	}
 
 	// Compass background at prog $20986 (32x27, raw 4-plane) and needle at prog $20B36
 	// (37 frames, 32x27 each, stride 432 bytes). Pre-composite background + needle.
@@ -486,6 +502,24 @@ void EclipseEngine::loadAssetsAtariFullGame() {
 		sprite->convertToInPlace(_gfx->_texturePixelFormat,
 			const_cast<byte *>(kBorderPalette), 16);
 
+	// Heartbeat/EKG animation: 5 frames, 16x11, at prog $20794, stride 96 bytes
+	// Drawn at (32, 138)
+	_heartbeatSprites.resize(5);
+	for (int i = 0; i < 5; i++) {
+		_heartbeatSprites[i] = loadAtariSTRawSprite(stream, 0x20794 + kHdr + i * 96, 1, 11);
+		_heartbeatSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+			const_cast<byte *>(kBorderPalette), 16);
+	}
+
+	// Water ripple animation: 9 frames, 32x9, at prog $27714, stride 144 bytes
+	// Mask at prog $27710. Drawn at (0, 28) in left border strip.
+	_waterSprites.resize(9);
+	for (int i = 0; i < 9; i++) {
+		_waterSprites[i] = loadAtariSTSprite(stream, 0x27710 + kHdr, 0x27714 + kHdr + i * 144, 2, 9);
+		_waterSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+			const_cast<byte *>(kBorderPalette), 16);
+	}
+
 	// Shooting crosshair sprites: 2 frames with mask, at prog $1CC26 and $1CDC0
 	// Frame 0: 32x25 (2 cols), frame 1: 48x25 (3 cols)
 	_shootSprites.resize(2);
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index c6589b090c2..e4b321c18ea 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -108,6 +108,9 @@ public:
 	Common::Array<Graphics::ManagedSurface *> _lanternLightSprites;  // 6 lantern light animation frames (32x6)
 	Common::Array<Graphics::ManagedSurface *> _lanternSwitchSprites; // 2 lantern on/off frames (32x23)
 	Common::Array<Graphics::ManagedSurface *> _shootSprites;         // 2 shooting crosshair frames (32x25, 48x25)
+	Common::Array<Graphics::ManagedSurface *> _ankhSprites;          // 5 ankh fade-in frames (16x15)
+	Common::Array<Graphics::ManagedSurface *> _waterSprites;         // 9 water ripple frames (32x9)
+	Common::Array<Graphics::ManagedSurface *> _heartbeatSprites;     // 5 heartbeat/EKG frames (16x11)
 	byte _compassLookup[72];  // direction-to-needle-frame lookup table
 
 	bool checkIfGameEnded() override;


Commit: 1d61c84029be808407f4cbf92ab6716474c392b7
    https://github.com/scummvm/scummvm/commit/1d61c84029be808407f4cbf92ab6716474c392b7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00

Commit Message:
FREESCAPE: on-screen control for eclipse in atari

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


diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index 4041921beba..b8fbd2f6630 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -31,6 +31,33 @@ namespace Freescape {
 
 void EclipseEngine::initAmigaAtari() {
 	_viewArea = Common::Rect(32, 16, 288, 118);
+
+	// On-screen control hotspots (from binary hotspot table at prog $869A)
+	// Right-side arrow buttons
+	_lookUpArea = Common::Rect(268, 133, 288, 153);
+	_lookDownArea = Common::Rect(290, 133, 310, 153);
+	_turnLeftArea = Common::Rect(268, 155, 288, 175);
+	_turnRightArea = Common::Rect(290, 155, 310, 175);
+	_uTurnArea = Common::Rect(268, 177, 288, 197);
+	_faceForwardArea = Common::Rect(290, 177, 310, 197);
+
+	// Left-side buttons
+	_moveBackwardArea = Common::Rect(9, 133, 29, 153);
+	_stepBackwardArea = Common::Rect(9, 155, 29, 175);
+	_interactArea = Common::Rect(31, 155, 51, 175);
+	_infoDisplayArea = Common::Rect(31, 133, 51, 153);
+
+	// Center/functional areas
+	_lanternArea = Common::Rect(57, 138, 75, 168);
+	_restArea = Common::Rect(85, 140, 127, 177);
+
+	// Status bar indicators
+	_stepSizeArea = Common::Rect(74, 117, 86, 129);
+	_heightArea = Common::Rect(222, 117, 234, 129);
+
+	// Save/load (menu screen)
+	_saveGameArea = Common::Rect(180, 36, 190, 47);
+	_loadGameArea = Common::Rect(180, 50, 190, 60);
 }
 
 /*void EclipseEngine::loadAssetsCPCFullGame() {
@@ -502,12 +529,12 @@ void EclipseEngine::loadAssetsAtariFullGame() {
 		sprite->convertToInPlace(_gfx->_texturePixelFormat,
 			const_cast<byte *>(kBorderPalette), 16);
 
-	// Heartbeat/EKG animation: 5 frames, 16x11, at prog $20794, stride 96 bytes
-	// Drawn at (32, 138)
-	_heartbeatSprites.resize(5);
+	// Sound ON/OFF toggle: 5 frames, 16x11, at prog $20794, stride 96 bytes
+	// Drawn at (32, 138) when sound is toggled
+	_soundToggleSprites.resize(5);
 	for (int i = 0; i < 5; i++) {
-		_heartbeatSprites[i] = loadAtariSTRawSprite(stream, 0x20794 + kHdr + i * 96, 1, 11);
-		_heartbeatSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
+		_soundToggleSprites[i] = loadAtariSTRawSprite(stream, 0x20794 + kHdr + i * 96, 1, 11);
+		_soundToggleSprites[i]->convertToInPlace(_gfx->_texturePixelFormat,
 			const_cast<byte *>(kBorderPalette), 16);
 	}
 
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 6b5d0e5ef5d..852df0197e2 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -543,6 +543,79 @@ void EclipseEngine::pressedKey(const int keycode) {
 	}
 }
 
+bool EclipseEngine::onScreenControls(Common::Point mouse) {
+	if (!isAmiga() && !isAtariST())
+		return false;
+
+	// Right-side arrow buttons
+	if (_lookUpArea.contains(mouse)) {
+		rotate(0, -5, 0);
+		return true;
+	} else if (_lookDownArea.contains(mouse)) {
+		rotate(0, 5, 0);
+		return true;
+	} else if (_turnLeftArea.contains(mouse)) {
+		rotate(-5, 0, 0);
+		return true;
+	} else if (_turnRightArea.contains(mouse)) {
+		rotate(5, 0, 0);
+		return true;
+	} else if (_uTurnArea.contains(mouse)) {
+		_yaw += 180;
+		updateCamera();
+		return true;
+	} else if (_faceForwardArea.contains(mouse)) {
+		pressedKey(kActionFaceForward);
+		return true;
+	}
+
+	// Left-side buttons (movement buttons just consume click, like Driller)
+	if (_moveBackwardArea.contains(mouse)) {
+		return true;
+	} else if (_stepBackwardArea.contains(mouse)) {
+		return true;
+	} else if (_interactArea.contains(mouse)) {
+		activate();
+		return true;
+	} else if (_infoDisplayArea.contains(mouse)) {
+		drawInfoMenu();
+		return true;
+	}
+
+	// Center/functional areas
+	if (_lanternArea.contains(mouse)) {
+		pressedKey(kActionToggleFlashlight);
+		return true;
+	} else if (_restArea.contains(mouse)) {
+		pressedKey(kActionRest);
+		return true;
+	}
+
+	// Status bar indicators
+	if (_stepSizeArea.contains(mouse)) {
+		pressedKey(kActionChangeStepSize);
+		return true;
+	} else if (_heightArea.contains(mouse)) {
+		pressedKey(kActionToggleRiseLower);
+		return true;
+	}
+
+	// Save/load
+	if (_saveGameArea.contains(mouse)) {
+		_gfx->setViewport(_fullscreenViewArea);
+		saveGameDialog();
+		_gfx->setViewport(_viewArea);
+		return true;
+	} else if (_loadGameArea.contains(mouse)) {
+		_gfx->setViewport(_fullscreenViewArea);
+		loadGameDialog();
+		_gfx->setViewport(_viewArea);
+		return true;
+	}
+
+	return false;
+}
+
 void EclipseEngine::releasedKey(const int keycode) {
 	if (keycode == kActionRiseOrFlyUp)
 		_resting = false;
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index e4b321c18ea..ddfe5e185b3 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -110,9 +110,29 @@ public:
 	Common::Array<Graphics::ManagedSurface *> _shootSprites;         // 2 shooting crosshair frames (32x25, 48x25)
 	Common::Array<Graphics::ManagedSurface *> _ankhSprites;          // 5 ankh fade-in frames (16x15)
 	Common::Array<Graphics::ManagedSurface *> _waterSprites;         // 9 water ripple frames (32x9)
-	Common::Array<Graphics::ManagedSurface *> _heartbeatSprites;     // 5 heartbeat/EKG frames (16x11)
+	Common::Array<Graphics::ManagedSurface *> _soundToggleSprites;   // 5 sound on/off toggle frames (16x11)
 	byte _compassLookup[72];  // direction-to-needle-frame lookup table
 
+	// Atari ST on-screen control hotspots (from binary hotspot table at prog $869A)
+	bool onScreenControls(Common::Point mouse) override;
+
+	Common::Rect _lookUpArea;
+	Common::Rect _lookDownArea;
+	Common::Rect _turnLeftArea;
+	Common::Rect _turnRightArea;
+	Common::Rect _uTurnArea;
+	Common::Rect _faceForwardArea;
+	Common::Rect _moveBackwardArea;
+	Common::Rect _stepBackwardArea;
+	Common::Rect _interactArea;
+	Common::Rect _infoDisplayArea;
+	Common::Rect _lanternArea;
+	Common::Rect _restArea;
+	Common::Rect _stepSizeArea;
+	Common::Rect _heightArea;
+	Common::Rect _saveGameArea;
+	Common::Rect _loadGameArea;
+
 	bool checkIfGameEnded() override;
 	void endGame() override;
 	void loadSoundsFx(Common::SeekableReadStream *file, int offset, int number) override;


Commit: d45d8d20ddffeaf67c6594218ad2044e4a4c9517
    https://github.com/scummvm/scummvm/commit/d45d8d20ddffeaf67c6594218ad2044e4a4c9517
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-23T11:39:31+01:00

Commit Message:
FREESCAPE: process indicator in eclipse in atari

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


diff --git a/engines/freescape/games/eclipse/atari.cpp b/engines/freescape/games/eclipse/atari.cpp
index b8fbd2f6630..1a361890019 100644
--- a/engines/freescape/games/eclipse/atari.cpp
+++ b/engines/freescape/games/eclipse/atari.cpp
@@ -326,13 +326,20 @@ void EclipseEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
 			Common::Rect(_eclipseSprites[frame]->w, _eclipseSprites[frame]->h));
 	}
 
-	// Shield energy jar: sprite blit at x=$80(128), y=$84(132) — from $11CD8/$11CE0
-	// 16 frames showing jar fill level (0-15)
-	if (_shieldSprites.size() >= 16) {
-		int shieldLevel = _gameStateVars[k8bitVariableShield] * 15 / _maxShield;
-		shieldLevel = CLIP(shieldLevel, 0, 15);
-		surface->copyRectToSurface(*_shieldSprites[shieldLevel], 128, 132,
-			Common::Rect(_shieldSprites[shieldLevel]->w, _shieldSprites[shieldLevel]->h));
+	// Eclipse progress indicator: sprite blit at x=$80(128), y=$84(132) — from $11CA0-$11CE0
+	// 16 frames showing sun being progressively eclipsed.
+	// Frame = countdown-based: frame 0 = full sun, frame 15 = nearly fully eclipsed.
+	if (_eclipseProgressSprites.size() >= 16) {
+		// Frame 0 = fully eclipsed, frame 15 = full sun
+		// progress: 1.0 at game start (full time left), 0.0 when time's up
+		int frame = 0;
+		if (_initialCountdown > 0 && _countdown > 0) {
+			float progress = float(_countdown) / float(_initialCountdown);
+			frame = (int)(15.0f * progress);
+		}
+		frame = CLIP(frame, 0, 15);
+		surface->copyRectToSurface(*_eclipseProgressSprites[frame], 128, 132,
+			Common::Rect(_eclipseProgressSprites[frame]->w, _eclipseProgressSprites[frame]->h));
 	}
 
 	// Ankh indicators at y=$B6(182), x = i*16 + 3 — from $11D88
@@ -453,12 +460,12 @@ void EclipseEngine::loadAssetsAtariFullGame() {
 	_eclipseSprites[0] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + kHdr, 1, 13);
 	_eclipseSprites[1] = loadAtariSTSprite(stream, 0x1D2BE + kHdr, 0x1D2C0 + 104 + kHdr, 1, 13);
 
-	// Shield energy bar sprites: 16 frames, 16x16 pixels
+	// Eclipse progress indicator: 16 frames, 16x16 pixels
 	// Descriptor at prog $1DA90 (1 col × 16 rows), mask at +6, pixels at +8
-	// Each frame = 128 bytes (16 rows × 4 words × 2 bytes)
-	_shieldSprites.resize(16);
+	// Frame 0 = full sun, frame 15 = nearly fully eclipsed. Each frame = 128 bytes.
+	_eclipseProgressSprites.resize(16);
 	for (int i = 0; i < 16; i++)
-		_shieldSprites[i] = loadAtariSTSprite(stream, 0x1DA96 + kHdr, 0x1DA98 + kHdr + i * 128, 1, 16);
+		_eclipseProgressSprites[i] = loadAtariSTSprite(stream, 0x1DA96 + kHdr, 0x1DA98 + kHdr + i * 128, 1, 16);
 
 	// Ankh indicator: 5 fade-in frames at prog $1B734, 16x15 (1 col, stride 120 bytes)
 	// Mask at prog $1B732. Frame 3 = fully visible ankh.
@@ -556,10 +563,10 @@ void EclipseEngine::loadAssetsAtariFullGame() {
 		sprite->convertToInPlace(_gfx->_texturePixelFormat,
 			const_cast<byte *>(kBorderPalette), 16);
 
-	// Convert eclipse and shield sprites from CLUT8 to target format using border palette
+	// Convert heart and eclipse progress sprites from CLUT8 to target format using border palette
 	for (auto &sprite : _eclipseSprites)
 		sprite->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
-	for (auto &sprite : _shieldSprites)
+	for (auto &sprite : _eclipseProgressSprites)
 		sprite->convertToInPlace(_gfx->_texturePixelFormat, const_cast<byte *>(kBorderPalette), 16);
 
 	_fontLoaded = true;
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index ddfe5e185b3..c8398c6828d 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -103,7 +103,7 @@ public:
 	// Atari ST UI sprites (extracted from binary, pre-converted to target format)
 	Font _fontScore; // Font B (10 score digit glyphs, 4-plane at $249BE)
 	Common::Array<Graphics::ManagedSurface *> _eclipseSprites; // 2 eclipse animation frames (16x13)
-	Common::Array<Graphics::ManagedSurface *> _shieldSprites;  // 16 shield level frames (16x16)
+	Common::Array<Graphics::ManagedSurface *> _eclipseProgressSprites;  // 16 eclipse animation frames (16x16)
 	Common::Array<Graphics::ManagedSurface *> _compassSprites; // 37 pre-composited compass frames (32x27)
 	Common::Array<Graphics::ManagedSurface *> _lanternLightSprites;  // 6 lantern light animation frames (32x6)
 	Common::Array<Graphics::ManagedSurface *> _lanternSwitchSprites; // 2 lantern on/off frames (32x23)




More information about the Scummvm-git-logs mailing list