[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