[Scummvm-git-logs] scummvm master -> e4f5bc30f914feb38e90cc90de0fe2b7f5443b2f
neuromancer
noreply at scummvm.org
Wed Mar 25 08:23:33 UTC 2026
This automated email contains information about 7 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
4bb2459153 FREESCAPE: make sure colors changes per area in the ui of castle cpc
94076fb676 FREESCAPE: riddle frame rendering for castle cpc
562a7df544 FREESCAPE: riddle frame rendering for castle amiga
cc2cc2bb24 FREESCAPE: load health indicator in castle amiga
fd609715b8 FREESCAPE: loaded more frames in castle amiga, including cursors
c6e7608745 FREESCAPE: added pulsating surface detection and rendering for dark and castle (amiga/atari)
e4f5bc30f9 FREESCAPE: added an option for WASD movement and shift for run
Commit: 4bb2459153f77a71bd8bfb17a4cee2aa82790114
https://github.com/scummvm/scummvm/commit/4bb2459153f77a71bd8bfb17a4cee2aa82790114
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:15+01:00
Commit Message:
FREESCAPE: make sure colors changes per area in the ui of castle cpc
Changed paths:
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
engines/freescape/games/castle/cpc.cpp
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index e6b202b7f94..0bb5fbb4bc7 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -83,6 +83,11 @@ CastleEngine::CastleEngine(OSystem *syst, const ADGameDescription *gd) : Freesca
_spiritsMeterIndicatorSideFrame = nullptr;
_strenghtBackgroundFrame = nullptr;
_strenghtBarFrame = nullptr;
+ _strenghtBackgroundCLUT8 = nullptr;
+ _strenghtBarCLUT8 = nullptr;
+ _spiritsMeterBgCLUT8 = nullptr;
+ _spiritsMeterIndCLUT8 = nullptr;
+ _keysBorderCLUT8 = nullptr;
_menu = nullptr;
_menuButtons = nullptr;
@@ -328,6 +333,120 @@ Common::Array<Graphics::ManagedSurface *> CastleEngine::loadFramesWithHeaderCPC(
return frames;
}
+void CastleEngine::convertCPCSprite(Graphics::ManagedSurface *clut8, Graphics::ManagedSurface *&argb, bool transparentInk0) {
+ if (argb) {
+ argb->free();
+ delete argb;
+ }
+ if (transparentInk0) {
+ // Ink 0 = value 0 (transparent for copyRectToSurfaceWithKey with back=0)
+ argb = new Graphics::ManagedSurface();
+ argb->create(clut8->w, clut8->h, _gfx->_texturePixelFormat);
+ argb->fillRect(Common::Rect(0, 0, clut8->w, clut8->h), 0);
+
+ byte palette[4 * 3];
+ clut8->grabPalette(palette, 0, 4);
+
+ for (int y = 0; y < clut8->h; y++) {
+ for (int x = 0; x < clut8->w; x++) {
+ byte idx = clut8->getPixel(x, y);
+ if (idx != 0) {
+ uint32 color = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
+ palette[idx * 3], palette[idx * 3 + 1], palette[idx * 3 + 2]);
+ argb->setPixel(x, y, color);
+ }
+ }
+ }
+ } else {
+ // Opaque: ink 0 = solid black, fully covers what's beneath
+ Graphics::Surface *converted = _gfx->convertImageFormatIfNecessary(clut8);
+ argb = new Graphics::ManagedSurface();
+ argb->copyFrom(*converted);
+ converted->free();
+ delete converted;
+ }
+}
+
+Graphics::ManagedSurface *CastleEngine::loadFrameWithHeaderCPCIndexed(Common::SeekableReadStream *file, int pos) {
+ file->seek(pos);
+ int w = file->readByte();
+ int h = file->readByte();
+ file->readByte(); // mask
+ file->readUint16LE(); // frameSize
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(w * 4, h, Graphics::PixelFormat::createFormatCLUT8());
+ surface->fillRect(Common::Rect(0, 0, w * 4, h), 0);
+ for (int y = 0; y < h; y++)
+ for (int col = 0; col < w; col++) {
+ byte cpc_byte = file->readByte();
+ for (int i = 0; i < 4; i++)
+ surface->setPixel(col * 4 + i, y, getCPCPixel(cpc_byte, i, true));
+ }
+ return surface;
+}
+
+Common::Array<Graphics::ManagedSurface *> CastleEngine::loadFramesWithHeaderCPCIndexed(Common::SeekableReadStream *file, int pos, int numFrames) {
+ file->seek(pos);
+ int w = file->readByte();
+ int h = file->readByte();
+ file->readByte(); // mask
+ file->readUint16LE(); // frameSize
+ Common::Array<Graphics::ManagedSurface *> frames;
+ for (int f = 0; f < numFrames; f++) {
+ Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+ surface->create(w * 4, h, Graphics::PixelFormat::createFormatCLUT8());
+ surface->fillRect(Common::Rect(0, 0, w * 4, h), 0);
+ for (int y = 0; y < h; y++)
+ for (int col = 0; col < w; col++) {
+ byte cpc_byte = file->readByte();
+ for (int i = 0; i < 4; i++)
+ surface->setPixel(col * 4 + i, y, getCPCPixel(cpc_byte, i, true));
+ }
+ frames.push_back(surface);
+ }
+ return frames;
+}
+
+void CastleEngine::updateCPCSpritesPalette() {
+ byte palette[4 * 3];
+ for (int c = 0; c < 4; c++) {
+ uint8 r, g, b;
+ _gfx->selectColorFromFourColorPalette(c, r, g, b);
+ palette[c * 3 + 0] = r;
+ palette[c * 3 + 1] = g;
+ palette[c * 3 + 2] = b;
+ }
+
+ if (_keysBorderCLUT8) {
+ _keysBorderCLUT8->setPalette(palette, 0, 4);
+ convertCPCSprite(_keysBorderCLUT8, _keysBorderFrames[0], true);
+ }
+ if (_spiritsMeterBgCLUT8) {
+ _spiritsMeterBgCLUT8->setPalette(palette, 0, 4);
+ convertCPCSprite(_spiritsMeterBgCLUT8, _spiritsMeterIndicatorBackgroundFrame);
+ }
+ if (_spiritsMeterIndCLUT8) {
+ _spiritsMeterIndCLUT8->setPalette(palette, 0, 4);
+ convertCPCSprite(_spiritsMeterIndCLUT8, _spiritsMeterIndicatorFrame, true);
+ }
+ if (_strenghtBackgroundCLUT8) {
+ _strenghtBackgroundCLUT8->setPalette(palette, 0, 4);
+ convertCPCSprite(_strenghtBackgroundCLUT8, _strenghtBackgroundFrame);
+ }
+ if (_strenghtBarCLUT8) {
+ _strenghtBarCLUT8->setPalette(palette, 0, 4);
+ convertCPCSprite(_strenghtBarCLUT8, _strenghtBarFrame);
+ }
+ for (int f = 0; f < (int)_strenghtWeightsCLUT8.size(); f++) {
+ _strenghtWeightsCLUT8[f]->setPalette(palette, 0, 4);
+ convertCPCSprite(_strenghtWeightsCLUT8[f], _strenghtWeightsFrames[f], true);
+ }
+ for (int f = 0; f < (int)_flagCLUT8.size(); f++) {
+ _flagCLUT8[f]->setPalette(palette, 0, 4);
+ convertCPCSprite(_flagCLUT8[f], _flagFrames[f]);
+ }
+}
+
Graphics::ManagedSurface *CastleEngine::loadFrameCPC(Common::SeekableReadStream *file, Graphics::ManagedSurface *surface, int width, int height, const uint32 *cpcPalette) {
for (int y = 0; y < height; y++) {
for (int col = 0; col < width; col++) {
@@ -487,6 +606,10 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
_gfx->clearColorPairArray();
swapPalette(areaID);
+
+ if (isCPC())
+ updateCPCSpritesPalette();
+
if (isDOS()) {
_gfx->_colorPair[_currentArea->_underFireBackgroundColor] = _currentArea->_extraColor[1];
_gfx->_colorPair[_currentArea->_usualBackgroundColor] = _currentArea->_extraColor[0];
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 97261220129..bea91416378 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -136,6 +136,21 @@ public:
Graphics::ManagedSurface *_endGameBackgroundFrame;
Graphics::ManagedSurface *_gameOverBackgroundFrame;
+ // CPC: CLUT8 versions of UI sprites (indexed by ink 0-3). On area change,
+ // we setPalette + convert to ARGB, like the border does in swapPalette.
+ Graphics::ManagedSurface *_strenghtBackgroundCLUT8;
+ Graphics::ManagedSurface *_strenghtBarCLUT8;
+ Common::Array<Graphics::ManagedSurface *> _strenghtWeightsCLUT8;
+ Graphics::ManagedSurface *_spiritsMeterBgCLUT8;
+ Graphics::ManagedSurface *_spiritsMeterIndCLUT8;
+ Graphics::ManagedSurface *_keysBorderCLUT8;
+ Common::Array<Graphics::ManagedSurface *> _flagCLUT8;
+ uint32 _cpcUIPalette[4]; // used by gate rendering
+ void convertCPCSprite(Graphics::ManagedSurface *clut8, Graphics::ManagedSurface *&argb, bool transparentInk0 = false);
+ Graphics::ManagedSurface *loadFrameWithHeaderCPCIndexed(Common::SeekableReadStream *file, int pos);
+ Common::Array<Graphics::ManagedSurface *> loadFramesWithHeaderCPCIndexed(Common::SeekableReadStream *file, int pos, int numFrames);
+ void updateCPCSpritesPalette();
+
Common::Array<byte> _modData; // Embedded ProTracker module (Amiga demo)
Common::Array<int> _keysCollected;
bool _useRockTravel;
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index cd9f8022774..91419e825ee 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -199,38 +199,61 @@ void CastleEngine::loadAssetsCPCFullGame() {
_background = loadFrame(&mountainsStream, background, backgroundWidth, backgroundHeight, front);
- // CPC UI Sprites - located at different offsets than ZX Spectrum!
- // CPC uses Mode 1 format (4 pixels per byte, 2 bits per pixel).
- // Sprite pixel values 0-3 are CPC ink numbers that map to the border palette.
- uint32 cpcPalette[4];
- for (int i = 0; i < 4; i++) {
- cpcPalette[i] = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
- kCPCPaletteCastleBorderData[i][0],
- kCPCPaletteCastleBorderData[i][1],
- kCPCPaletteCastleBorderData[i][2]);
+ // CPC UI Sprites stored as CLUT8 (indexed by ink 0-3).
+ // On real CPC hardware, the 4-color palette changes per area, automatically
+ // recoloring everything. We store CLUT8 sprites and setPalette + convert
+ // when the area changes, just like the border does in swapPalette.
+ _keysBorderCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x2362);
+ _spiritsMeterBgCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x2383);
+ _spiritsMeterIndCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x2408);
+ _strenghtBackgroundCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x242D);
+ _strenghtBarCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x2531);
+ _strenghtWeightsCLUT8 = loadFramesWithHeaderCPCIndexed(&file, 0x2569, 4);
+ _flagCLUT8 = loadFramesWithHeaderCPCIndexed(&file, 0x2654, 4);
+
+ // Set initial border palette, convert to ARGB, and populate the drawing surfaces
+ {
+ byte initPalette[4 * 3];
+ for (int c = 0; c < 4; c++) {
+ initPalette[c * 3 + 0] = kCPCPaletteCastleBorderData[c][0];
+ initPalette[c * 3 + 1] = kCPCPaletteCastleBorderData[c][1];
+ initPalette[c * 3 + 2] = kCPCPaletteCastleBorderData[c][2];
+ }
+ _keysBorderCLUT8->setPalette(initPalette, 0, 4);
+ _spiritsMeterBgCLUT8->setPalette(initPalette, 0, 4);
+ _spiritsMeterIndCLUT8->setPalette(initPalette, 0, 4);
+ _strenghtBackgroundCLUT8->setPalette(initPalette, 0, 4);
+ _strenghtBarCLUT8->setPalette(initPalette, 0, 4);
+ for (auto *s : _strenghtWeightsCLUT8)
+ s->setPalette(initPalette, 0, 4);
+ for (auto *s : _flagCLUT8)
+ s->setPalette(initPalette, 0, 4);
+
+ for (int i = 0; i < 4; i++)
+ _cpcUIPalette[i] = _gfx->_texturePixelFormat.ARGBToColor(0xFF,
+ kCPCPaletteCastleBorderData[i][0],
+ kCPCPaletteCastleBorderData[i][1],
+ kCPCPaletteCastleBorderData[i][2]);
+
+ Graphics::ManagedSurface *tmp = nullptr;
+ convertCPCSprite(_keysBorderCLUT8, tmp, true);
+ _keysBorderFrames.push_back(tmp);
+ convertCPCSprite(_spiritsMeterBgCLUT8, _spiritsMeterIndicatorBackgroundFrame);
+ convertCPCSprite(_spiritsMeterIndCLUT8, _spiritsMeterIndicatorFrame, true);
+ convertCPCSprite(_strenghtBackgroundCLUT8, _strenghtBackgroundFrame);
+ convertCPCSprite(_strenghtBarCLUT8, _strenghtBarFrame);
+ for (int f = 0; f < 4; f++) {
+ tmp = nullptr;
+ convertCPCSprite(_strenghtWeightsCLUT8[f], tmp, true);
+ _strenghtWeightsFrames.push_back(tmp);
+ }
+ for (int f = 0; f < 4; f++) {
+ tmp = nullptr;
+ convertCPCSprite(_flagCLUT8[f], tmp);
+ _flagFrames.push_back(tmp);
+ }
}
- // Keys Border: CPC offset 0x2362 (8x14 px, 1 frame - matches ZX key_sprite)
- _keysBorderFrames.push_back(loadFrameWithHeaderCPC(&file, 0x2362, cpcPalette));
-
- // Spirit Meter Background: CPC offset 0x2383 (64x8 px - matches ZX spirit_meter_bg)
- _spiritsMeterIndicatorBackgroundFrame = loadFrameWithHeaderCPC(&file, 0x2383, cpcPalette);
-
- // Spirit Meter Indicator: CPC offset 0x2408 (16x8 px - matches ZX spirit_meter_indicator)
- _spiritsMeterIndicatorFrame = loadFrameWithHeaderCPC(&file, 0x2408, cpcPalette);
-
- // Strength Background: CPC offset 0x242D (68x15 px - matches ZX strength_bg)
- _strenghtBackgroundFrame = loadFrameWithHeaderCPC(&file, 0x242D, cpcPalette);
-
- // Strength Bar: CPC offset 0x2531 (68x3 px - matches ZX strength_bar)
- _strenghtBarFrame = loadFrameWithHeaderCPC(&file, 0x2531, cpcPalette);
-
- // Strength Weights: CPC offset 0x2569 (4x15 px, 4 frames - matches ZX weight_sprite w=1,h=15)
- _strenghtWeightsFrames = loadFramesWithHeaderCPC(&file, 0x2569, 4, cpcPalette);
-
- // Flag Animation: CPC offset 0x2654 (16x9 px, 4 frames)
- _flagFrames = loadFramesWithHeaderCPC(&file, 0x2654, 4, cpcPalette);
-
// Gate image (portcullis) for game start/end animation.
// The CPC gate is NOT a pre-rendered bitmap; it is procedurally generated
// from small pixel pattern tables stored at file offset 0x75EF-0x764C.
@@ -280,13 +303,13 @@ void CastleEngine::loadAssetsCPCFullGame() {
for (int p = 0; p < 2; p++) {
int ink = getCPCPixel(kGateLastRow[0], p, true);
if (ink)
- _gameOverBackgroundFrame->setPixel(bx + p, y, cpcPalette[ink]);
+ _gameOverBackgroundFrame->setPixel(bx + p, y, _cpcUIPalette[ink]);
}
// Right edge byte: pixels 2,3 from pattern
for (int p = 2; p < 4; p++) {
int ink = getCPCPixel(kGateLastRow[1], p, true);
if (ink)
- _gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, cpcPalette[ink]);
+ _gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, _cpcUIPalette[ink]);
}
}
} else {
@@ -324,7 +347,7 @@ void CastleEngine::loadAssetsCPCFullGame() {
for (int p = 0; p < 4; p++) {
int ink = getCPCPixel(cpcByte, p, true);
if (ink)
- _gameOverBackgroundFrame->setPixel(bx + bi * 4 + p, y, cpcPalette[ink]);
+ _gameOverBackgroundFrame->setPixel(bx + bi * 4 + p, y, _cpcUIPalette[ink]);
}
}
}
@@ -336,13 +359,13 @@ void CastleEngine::loadAssetsCPCFullGame() {
for (int p = 0; p < 2; p++) {
int ink = getCPCPixel(kGateInterBar[patIdx][0], p, true);
if (ink)
- _gameOverBackgroundFrame->setPixel(bx + p, y, cpcPalette[ink]);
+ _gameOverBackgroundFrame->setPixel(bx + p, y, _cpcUIPalette[ink]);
}
// Right edge (byte 5): pixels 2,3 from pattern byte 1
for (int p = 2; p < 4; p++) {
int ink = getCPCPixel(kGateInterBar[patIdx][1], p, true);
if (ink)
- _gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, cpcPalette[ink]);
+ _gameOverBackgroundFrame->setPixel(bx + (kBytesPerCol - 1) * 4 + p, y, _cpcUIPalette[ink]);
}
}
}
Commit: 94076fb6760945ef4446744c522729a72a44b9be
https://github.com/scummvm/scummvm/commit/94076fb6760945ef4446744c522729a72a44b9be
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00
Commit Message:
FREESCAPE: riddle frame rendering for castle cpc
Changed paths:
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
engines/freescape/games/castle/cpc.cpp
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 0bb5fbb4bc7..09d12e5b5a0 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -94,6 +94,7 @@ CastleEngine::CastleEngine(OSystem *syst, const ADGameDescription *gd) : Freesca
_riddleTopFrame = nullptr;
_riddleBottomFrame = nullptr;
_riddleBackgroundFrame = nullptr;
+ _riddleNailFrame = nullptr;
_endGameThroneFrame = nullptr;
_endGameBackgroundFrame = nullptr;
@@ -1375,6 +1376,9 @@ void CastleEngine::drawFullscreenRiddleAndWait(uint16 riddle) {
case Common::kRenderZX:
frontColor = 7;
break;
+ case Common::kRenderCPC:
+ frontColor = _gfx->_inkColor;
+ break;
default:
break;
}
@@ -1438,7 +1442,11 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
if (isDOS()) {
x = 40;
y = 34;
- } else if (isSpectrum() || isCPC()) {
+ } else if (isCPC()) {
+ x = 40;
+ y = 46;
+ maxWidth = 139;
+ } else if (isSpectrum()) {
x = 64;
y = 37;
} else if (isAmiga()) {
@@ -1446,6 +1454,18 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
y = 33;
maxWidth = 139;
}
+ // Draw rope lines and nail above the riddle frame (CPC)
+ if (isCPC() && _riddleNailFrame) {
+ int nailX = x + (_viewArea.width() - _riddleNailFrame->w) / 2;
+ int nailY = _viewArea.top + 2;
+ int nailCenterX = nailX + _riddleNailFrame->w / 2;
+ int nailCenterY = nailY + _riddleNailFrame->h / 2;
+ // Rope lines first, then nail on top
+ surface->drawLine(nailCenterX, nailCenterY, x, y, front);
+ surface->drawLine(nailCenterX, nailCenterY, x + _viewArea.width() - 1, y, front);
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_riddleNailFrame, nailX, nailY, Common::Rect(0, 0, _riddleNailFrame->w, _riddleNailFrame->h), 0);
+ }
+
// Draw riddle frame borders (if available)
if (_riddleTopFrame) {
surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, x, y, Common::Rect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h));
@@ -1468,7 +1488,10 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
if (isDOS()) {
x = 38;
y = 33;
- } else if (isSpectrum() || isCPC()) {
+ } else if (isCPC()) {
+ x = 40;
+ y = 33;
+ } else if (isSpectrum()) {
x = 64;
y = 36;
} else if (isAmiga()) {
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index bea91416378..ba04558213b 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -131,6 +131,7 @@ public:
Graphics::ManagedSurface *_riddleTopFrame;
Graphics::ManagedSurface *_riddleBackgroundFrame;
Graphics::ManagedSurface *_riddleBottomFrame;
+ Graphics::ManagedSurface *_riddleNailFrame;
Graphics::ManagedSurface *_endGameThroneFrame;
Graphics::ManagedSurface *_endGameBackgroundFrame;
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index 91419e825ee..4c92dc765ca 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -117,6 +117,31 @@ byte mountainsData[288] {
+// Expand a 5-byte CPC riddle frame row definition into a 240-pixel CLUT8 row.
+// Format: 2 left border bytes + 1 fill byte (repeated 54Ã) + 2 right border bytes,
+// padded with 1 black byte on each side.
+void expandRiddleRow(const byte *src, Graphics::ManagedSurface *surface, int y) {
+ int x = 0;
+ for (int p = 0; p < 4; p++)
+ surface->setPixel(x++, y, 0);
+ for (int b = 0; b < 2; b++) {
+ byte cpcByte = src[b];
+ for (int p = 0; p < 4; p++)
+ surface->setPixel(x++, y, getCPCPixel(cpcByte, p, true));
+ }
+ byte fillByte = src[2];
+ for (int b = 0; b < 54; b++)
+ for (int p = 0; p < 4; p++)
+ surface->setPixel(x++, y, getCPCPixel(fillByte, p, true));
+ for (int b = 0; b < 2; b++) {
+ byte cpcByte = src[3 + b];
+ for (int p = 0; p < 4; p++)
+ surface->setPixel(x++, y, getCPCPixel(cpcByte, p, true));
+ }
+ for (int p = 0; p < 4; p++)
+ surface->setPixel(x++, y, 0);
+}
+
void CastleEngine::loadAssetsCPCFullGame() {
Common::File file;
uint8 r, g, b;
@@ -254,6 +279,65 @@ void CastleEngine::loadAssetsCPCFullGame() {
}
}
+ // Riddle frame graphics at file offset 0x26E9 (CPC addr 0x31A9).
+ // The CPC draw function expands each 5-byte row to:
+ // 1 black + 2 left border + 54Ãfill + 2 right border + 1 black = 60 bytes = 240 pixels.
+ // Structure: 7 top rows + 1 body row (repeated) + 7 bottom rows (top reversed).
+ {
+ static const int kRiddleFrameOffset = 0x26E9;
+ static const int kTopRows = 7;
+ static const int kRowWidth = 240; // pixels
+
+ file.seek(kRiddleFrameOffset);
+ byte riddleData[40];
+ file.read(riddleData, 40);
+
+ // Top frame: 7 rows
+ Graphics::ManagedSurface *topCLUT8 = new Graphics::ManagedSurface();
+ topCLUT8->create(kRowWidth, kTopRows, Graphics::PixelFormat::createFormatCLUT8());
+ topCLUT8->fillRect(Common::Rect(0, 0, kRowWidth, kTopRows), 0);
+ for (int row = 0; row < kTopRows; row++)
+ expandRiddleRow(&riddleData[row * 5], topCLUT8, row);
+
+ // Background: 1 row (the body row after the 7 top rows)
+ Graphics::ManagedSurface *bgCLUT8 = new Graphics::ManagedSurface();
+ bgCLUT8->create(kRowWidth, 1, Graphics::PixelFormat::createFormatCLUT8());
+ bgCLUT8->fillRect(Common::Rect(0, 0, kRowWidth, 1), 0);
+ expandRiddleRow(&riddleData[kTopRows * 5], bgCLUT8, 0);
+
+ // Bottom frame: 7 rows (top data in reverse order)
+ Graphics::ManagedSurface *bottomCLUT8 = new Graphics::ManagedSurface();
+ bottomCLUT8->create(kRowWidth, kTopRows, Graphics::PixelFormat::createFormatCLUT8());
+ bottomCLUT8->fillRect(Common::Rect(0, 0, kRowWidth, kTopRows), 0);
+ for (int row = 0; row < kTopRows; row++)
+ expandRiddleRow(&riddleData[(kTopRows - 1 - row) * 5], bottomCLUT8, row);
+
+ // Set palette and convert
+ byte initPalette[4 * 3];
+ for (int c = 0; c < 4; c++) {
+ initPalette[c * 3 + 0] = kCPCPaletteCastleBorderData[c][0];
+ initPalette[c * 3 + 1] = kCPCPaletteCastleBorderData[c][1];
+ initPalette[c * 3 + 2] = kCPCPaletteCastleBorderData[c][2];
+ }
+ topCLUT8->setPalette(initPalette, 0, 4);
+ bgCLUT8->setPalette(initPalette, 0, 4);
+ bottomCLUT8->setPalette(initPalette, 0, 4);
+
+ convertCPCSprite(topCLUT8, _riddleTopFrame);
+ convertCPCSprite(bgCLUT8, _riddleBackgroundFrame);
+ convertCPCSprite(bottomCLUT8, _riddleBottomFrame);
+
+ // Nail sprite at file offset 0x2711 (8Ã7 pixels, drawn above the frame)
+ Graphics::ManagedSurface *nailCLUT8 = loadFrameWithHeaderCPCIndexed(&file, 0x2711);
+ nailCLUT8->setPalette(initPalette, 0, 4);
+ convertCPCSprite(nailCLUT8, _riddleNailFrame);
+
+ delete topCLUT8;
+ delete bgCLUT8;
+ delete bottomCLUT8;
+ delete nailCLUT8;
+ }
+
// Gate image (portcullis) for game start/end animation.
// The CPC gate is NOT a pre-rendered bitmap; it is procedurally generated
// from small pixel pattern tables stored at file offset 0x75EF-0x764C.
Commit: 562a7df544b281a1819eda95e79061e829b2544c
https://github.com/scummvm/scummvm/commit/562a7df544b281a1819eda95e79061e829b2544c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00
Commit Message:
FREESCAPE: riddle frame rendering for castle amiga
Changed paths:
engines/freescape/games/castle/amiga.cpp
engines/freescape/games/castle/castle.cpp
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index e8399d42666..6dc21dd004d 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -138,22 +138,25 @@ void CastleEngine::loadAssetsAmigaDemo() {
file.seek(0x11eec);
Common::Array<Graphics::ManagedSurface *> chars;
+ Common::Array<Graphics::ManagedSurface *> charsRiddle;
for (int i = 0; i < 90; i++) {
Graphics::ManagedSurface *img = loadFrameFromPlanes(&file, 8, 8);
- //Graphics::ManagedSurface *imgRiddle = new Graphics::ManagedSurface();
- //imgRiddle->copyFrom(*img);
+ Graphics::ManagedSurface *imgRiddle = new Graphics::ManagedSurface();
+ imgRiddle->copyFrom(*img);
chars.push_back(img);
chars[i]->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
- //charsRiddle.push_back(imgRiddle);
- //charsRiddle[i]->convertToInPlace(_gfx->_texturePixelFormat, (byte *)&kEGARiddleFontPalette, 16);
+ charsRiddle.push_back(imgRiddle);
+ charsRiddle[i]->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
}
- // 0x1356c
_font = Font(chars);
_font.setCharWidth(9);
+ _fontRiddle = Font(charsRiddle);
+ _fontRiddle.setCharWidth(9);
+
load8bitBinary(&file, 0x162a6, 16);
for (int i = 0; i < 3; i++) {
debugC(1, kFreescapeDebugParser, "Continue to parse area index %d at offset %x", _areaMap.size() + i + 1, (int)file.pos());
@@ -220,15 +223,36 @@ void CastleEngine::loadAssetsAmigaDemo() {
_flagFrames.push_back(frame);
}
+ // Riddle mask (memory 0x3C6DA, file 0x3C6F6): 16 words, one per 16-pixel column.
+ // Applied per-pixel: frame_pixel = (mask_bit == 1) ? frame_pixel : 0.
+ // Same mask for every row. Trims the frame edges for proper compositing.
+ file.seek(0x3c6f6);
+ uint16 riddleMask[16];
+ for (int i = 0; i < 16; i++)
+ riddleMask[i] = file.readUint16BE();
+
// Riddle frames (memory 0x3C6FA: top 20 rows + bg 1 row + bottom 8 rows, 256px wide)
file.seek(0x3c716);
_riddleTopFrame = loadFrameFromPlanesInterleaved(&file, 16, 20);
- _riddleTopFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
-
_riddleBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 16, 1);
- _riddleBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
-
_riddleBottomFrame = loadFrameFromPlanesInterleaved(&file, 16, 8);
+
+ // Apply mask to CLUT8 frames before palette conversion
+ Graphics::ManagedSurface *riddleFrames[] = {_riddleTopFrame, _riddleBackgroundFrame, _riddleBottomFrame};
+ for (int f = 0; f < 3; f++) {
+ Graphics::ManagedSurface *frame = riddleFrames[f];
+ for (int y = 0; y < frame->h; y++) {
+ for (int x = 0; x < frame->w; x++) {
+ int col = x / 16;
+ int bit = 15 - (x % 16);
+ if (!(riddleMask[col] & (1 << bit)))
+ frame->setPixel(x, y, 0);
+ }
+ }
+ }
+
+ _riddleTopFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
+ _riddleBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
_riddleBottomFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastleRiddlePalette, 16);
// Castle gate (game over background frame)
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 09d12e5b5a0..079966ef7fb 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -1466,19 +1466,34 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_riddleNailFrame, nailX, nailY, Common::Rect(0, 0, _riddleNailFrame->w, _riddleNailFrame->h), 0);
}
- // Draw riddle frame borders (if available)
+ // Draw riddle frame borders (if available), clipped to viewport
if (_riddleTopFrame) {
- surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, x, y, Common::Rect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h));
+ Common::Rect srcRect(0, 0, _riddleTopFrame->w, _riddleTopFrame->h);
+ Common::Rect destRect(x, y, x + _riddleTopFrame->w, y + _riddleTopFrame->h);
+ destRect.clip(_viewArea);
+ srcRect = Common::Rect(destRect.left - x, destRect.top - y, destRect.right - x, destRect.bottom - y);
+ if (srcRect.isValidRect() && !srcRect.isEmpty())
+ surface->copyRectToSurface((const Graphics::Surface)*_riddleTopFrame, destRect.left, destRect.top, srcRect);
y += _riddleTopFrame->h;
}
if (_riddleBackgroundFrame) {
for (; y < maxWidth;) {
- surface->copyRectToSurface((const Graphics::Surface)*_riddleBackgroundFrame, x, y, Common::Rect(0, 0, _riddleBackgroundFrame->w, _riddleBackgroundFrame->h));
+ Common::Rect srcRect(0, 0, _riddleBackgroundFrame->w, _riddleBackgroundFrame->h);
+ Common::Rect destRect(x, y, x + _riddleBackgroundFrame->w, y + _riddleBackgroundFrame->h);
+ destRect.clip(_viewArea);
+ srcRect = Common::Rect(destRect.left - x, destRect.top - y, destRect.right - x, destRect.bottom - y);
+ if (srcRect.isValidRect() && !srcRect.isEmpty())
+ surface->copyRectToSurface((const Graphics::Surface)*_riddleBackgroundFrame, destRect.left, destRect.top, srcRect);
y += _riddleBackgroundFrame->h;
}
}
if (_riddleBottomFrame) {
- surface->copyRectToSurface((const Graphics::Surface)*_riddleBottomFrame, x, maxWidth, Common::Rect(0, 0, _riddleBottomFrame->w, _riddleBottomFrame->h - 1));
+ Common::Rect srcRect(0, 0, _riddleBottomFrame->w, _riddleBottomFrame->h - 1);
+ Common::Rect destRect(x, maxWidth, x + _riddleBottomFrame->w, maxWidth + _riddleBottomFrame->h - 1);
+ destRect.clip(_viewArea);
+ srcRect = Common::Rect(destRect.left - x, destRect.top - maxWidth, destRect.right - x, destRect.bottom - maxWidth);
+ if (srcRect.isValidRect() && !srcRect.isEmpty())
+ surface->copyRectToSurface((const Graphics::Surface)*_riddleBottomFrame, destRect.left, destRect.top, srcRect);
}
Common::Array<RiddleText> riddleMessages = _riddleList[riddle]._lines;
@@ -1510,7 +1525,7 @@ void CastleEngine::drawRiddle(uint16 riddle, uint32 front, uint32 back, Graphics
void CastleEngine::drawRiddleStringInSurface(const Common::String &str, int x, int y, uint32 fontColor, uint32 backColor, Graphics::Surface *surface) {
Common::String ustr = str;
ustr.toUppercase();
- if (isDOS()) {
+ if (isDOS() || isAmiga() || isAtariST()) {
_fontRiddle.setBackground(backColor);
_fontRiddle.drawString(surface, ustr, x, y, _screenW, fontColor);
} else {
Commit: cc2cc2bb24fd6ac65199c55284cfe956f12bf90e
https://github.com/scummvm/scummvm/commit/cc2cc2bb24fd6ac65199c55284cfe956f12bf90e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00
Commit Message:
FREESCAPE: load health indicator in castle amiga
Changed paths:
engines/freescape/games/castle/amiga.cpp
engines/freescape/games/castle/castle.cpp
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 6dc21dd004d..658e80eb860 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -207,6 +207,19 @@ void CastleEngine::loadAssetsAmigaDemo() {
_spiritsMeterIndicatorFrame = loadFrameFromPlanesInterleaved(&file, 1, 10);
_spiritsMeterIndicatorFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ // Strength weight sprites (file 0x395F2, 1 word x 14 rows x 4 frames)
+ file.seek(0x395f2);
+ for (int i = 0; i < 4; i++) {
+ Graphics::ManagedSurface *frame = loadFrameFromPlanesInterleaved(&file, 1, 14);
+ frame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _strenghtWeightsFrames.push_back(frame);
+ }
+
+ // Strength background with bar (file 0x397B2, 5 words x 20 rows)
+ //file.seek(0x397b2);
+ //_strenghtBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 5, 4);
+ //_strenghtBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+
// Key sprites (memory 0x3C096, 12 frames, 16x7 each, interleaved 4-plane)
file.seek(0x3c0b2);
for (int i = 0; i < 12; i++) {
@@ -393,6 +406,9 @@ void CastleEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
Common::Rect(0, 0, _flagFrames[flagFrameIndex]->w, _flagFrames[flagFrameIndex]->h));
}
+ // Draw energy meter (strength) - background placed at (0, 154) to match border
+ drawEnergyMeter(surface, Common::Point(40, 158));
+
// Draw spirit meter
if (_spiritsMeterIndicatorBackgroundFrame)
surface->copyRectToSurface((const Graphics::Surface)*_spiritsMeterIndicatorBackgroundFrame, 128, 160,
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 079966ef7fb..3c758f5a290 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -1535,17 +1535,13 @@ void CastleEngine::drawRiddleStringInSurface(const Common::String &str, int x, i
}
void CastleEngine::drawEnergyMeter(Graphics::Surface *surface, Common::Point origin) {
- if (!_strenghtBackgroundFrame)
- return;
-
- surface->copyRectToSurface((const Graphics::Surface)*_strenghtBackgroundFrame, origin.x, origin.y, Common::Rect(0, 0, _strenghtBackgroundFrame->w, _strenghtBackgroundFrame->h));
- if (!_strenghtBarFrame)
- return;
+ if (_strenghtBackgroundFrame)
+ surface->copyRectToSurface((const Graphics::Surface)*_strenghtBackgroundFrame, origin.x, origin.y, Common::Rect(0, 0, _strenghtBackgroundFrame->w, _strenghtBackgroundFrame->h));
uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
uint32 back = 0;
- if (isDOS())
+ if (isDOS() || isAmiga() || isAtariST())
back = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
int strength = _gameStateVars[k8bitVariableShield];
@@ -1555,14 +1551,16 @@ void CastleEngine::drawEnergyMeter(Graphics::Surface *surface, Common::Point ori
Common::Point barFrameOrigin = origin;
- if (isDOS())
- barFrameOrigin += Common::Point(5, 6 + extraYOffset);
- else if (isSpectrum())
- barFrameOrigin += Common::Point(0, 6 + extraYOffset);
- else if (isCPC())
- barFrameOrigin += Common::Point(0, 6 + extraYOffset);
+ if (_strenghtBarFrame) {
+ if (isDOS())
+ barFrameOrigin += Common::Point(5, 6 + extraYOffset);
+ else if (isSpectrum())
+ barFrameOrigin += Common::Point(0, 6 + extraYOffset);
+ else if (isCPC())
+ barFrameOrigin += Common::Point(0, 6 + extraYOffset);
- surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtBarFrame, barFrameOrigin.x, barFrameOrigin.y, Common::Rect(0, 0, _strenghtBarFrame->w, _strenghtBarFrame->h), black);
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_strenghtBarFrame, barFrameOrigin.x, barFrameOrigin.y, Common::Rect(0, 0, _strenghtBarFrame->w, _strenghtBarFrame->h), black);
+ }
Common::Point weightPoint;
int frameIdx = -1;
@@ -1586,6 +1584,10 @@ void CastleEngine::drawEnergyMeter(Graphics::Surface *surface, Common::Point ori
weightStep = 3;
weightOffset = 5;
rightWeightPos = 63;
+ } else if (isAmiga() || isAtariST()) {
+ weightStep = 3;
+ weightOffset = 10;
+ rightWeightPos = 62;
} else { // DOS
weightStep = 3;
weightOffset = 10;
Commit: fd609715b8ff68400b918cb8bf6e6b6ba9004d91
https://github.com/scummvm/scummvm/commit/fd609715b8ff68400b918cb8bf6e6b6ba9004d91
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00
Commit Message:
FREESCAPE: loaded more frames in castle amiga, including cursors
Changed paths:
engines/freescape/games/castle/amiga.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 658e80eb860..79932cf223a 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -21,6 +21,7 @@
#include "common/file.h"
#include "common/memstream.h"
+#include "graphics/cursorman.h"
#include "audio/mods/protracker.h"
@@ -136,6 +137,7 @@ void CastleEngine::loadAssetsAmigaDemo() {
loadMessagesVariableSize(&file, 0x8bb2, 178);
loadRiddles(&file, 0x96c8 - 2 - 19 * 2, 19);
+
file.seek(0x11eec);
Common::Array<Graphics::ManagedSurface *> chars;
Common::Array<Graphics::ManagedSurface *> charsRiddle;
@@ -220,7 +222,9 @@ void CastleEngine::loadAssetsAmigaDemo() {
//_strenghtBackgroundFrame = loadFrameFromPlanesInterleaved(&file, 5, 4);
//_strenghtBackgroundFrame->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
- // Key sprites (memory 0x3C096, 12 frames, 16x7 each, interleaved 4-plane)
+ // Eye icon sprites (memory 0x3C096, 12 frames, 16x7 each, interleaved 4-plane)
+ // Used for strength/compass display at screen (224, 164). Header at 0x3C08E.
+ // TODO: load as separate eye icon member, not _keysBorderFrames
file.seek(0x3c0b2);
for (int i = 0; i < 12; i++) {
Graphics::ManagedSurface *frame = loadFrameFromPlanesInterleaved(&file, 1, 7);
@@ -228,6 +232,79 @@ void CastleEngine::loadAssetsAmigaDemo() {
_keysBorderFrames.push_back(frame);
}
+ // Crawl/Walk/Run + Sound indicators (memory 0x3838A, file 0x383A6, 5 frames, 48x12)
+ // Header (6 bytes) + mask (3 words = 6 bytes) + graphics.
+ // From assembly: frames 0-2 = crawl/walk/run at (96,118), frames 3-4 = sound off/on at (96,103)
+ file.seek(0x383a6 + 6 + 6); // skip header + mask
+ {
+ _menuCrawlIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+ _menuCrawlIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _menuWalkIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+ _menuWalkIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _menuRunIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+ _menuRunIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _menuFxOffIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+ _menuFxOffIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ _menuFxOnIndicator = loadFrameFromPlanesInterleaved(&file, 3, 12);
+ _menuFxOnIndicator->convertToInPlace(_gfx->_texturePixelFormat, (byte *)kAmigaCastlePalette, 16);
+ }
+
+ // Mouse pointer from paired sprites at mem $22E/$276 (file 0x24A/0x292).
+ // SPR0 at $22E + SPR1 at $276 form the diagonal arrow cursor.
+ // Each: 2 control words + 16 data rows (p0,p1 interleaved) + end marker = 72 bytes.
+ // SPR0 contributes color bits 0-1, SPR1 contributes bits 2-3 (4-bit combined).
+ {
+ _cursorW = 16;
+ _cursorH = 16;
+ _cursorData = new byte[16 * 16];
+ memset(_cursorData, 0, 16 * 16);
+ // Read SPR0 (bits 0-1)
+ file.seek(0x24A + 4); // skip control
+ for (int row = 0; row < 16; row++) {
+ uint16 p0 = file.readUint16BE();
+ uint16 p1 = file.readUint16BE();
+ for (int bit = 0; bit < 16; bit++) {
+ byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+ _cursorData[row * 16 + bit] = c;
+ }
+ }
+ // Overlay SPR1 (bits 2-3)
+ file.seek(0x292 + 4); // skip control
+ for (int row = 0; row < 16; row++) {
+ uint16 p0 = file.readUint16BE();
+ uint16 p1 = file.readUint16BE();
+ for (int bit = 0; bit < 16; bit++) {
+ byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+ _cursorData[row * 16 + bit] |= (c << 2);
+ }
+ }
+ }
+
+ // Crosshair pointer from paired sprites at mem $19E/$1E6 (file 0x1BA/0x202).
+ // Used outside the view area. Same format as diagonal arrow.
+ {
+ _crosshairData = new byte[16 * 16];
+ memset(_crosshairData, 0, 16 * 16);
+ file.seek(0x1BA + 4);
+ for (int row = 0; row < 16; row++) {
+ uint16 p0 = file.readUint16BE();
+ uint16 p1 = file.readUint16BE();
+ for (int bit = 0; bit < 16; bit++) {
+ byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+ _crosshairData[row * 16 + bit] = c;
+ }
+ }
+ file.seek(0x202 + 4);
+ for (int row = 0; row < 16; row++) {
+ uint16 p0 = file.readUint16BE();
+ uint16 p1 = file.readUint16BE();
+ for (int bit = 0; bit < 16; bit++) {
+ byte c = ((p0 >> (15 - bit)) & 1) | (((p1 >> (15 - bit)) & 1) << 1);
+ _crosshairData[row * 16 + bit] |= (c << 2);
+ }
+ }
+ }
+
// Flag animation (memory 0x3C340, 5 frames, 32x11 each, interleaved 4-plane)
file.seek(0x3c35c);
for (int i = 0; i < 5; i++) {
@@ -391,13 +468,7 @@ void CastleEngine::drawAmigaAtariSTUI(Graphics::Surface *surface) {
drawStringInSurface(_currentArea->_name, 97, 182, 0, 0, surface);
uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00);
- // Draw last collected key at (224, 164)
- if (!_keysCollected.empty() && !_keysBorderFrames.empty()) {
- int k = int(_keysCollected.size()) - 1;
- if (k < int(_keysBorderFrames.size()))
- surface->copyRectToSurfaceWithKey((const Graphics::Surface)*_keysBorderFrames[k], 224, 164,
- Common::Rect(0, 0, _keysBorderFrames[k]->w, _keysBorderFrames[k]->h), black);
- }
+ // TODO: Draw collected keys - key sprites location in binary still unknown
// Draw flag animation at (288, 5)
if (!_flagFrames.empty()) {
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 3c758f5a290..0c6d2495015 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -90,6 +90,10 @@ CastleEngine::CastleEngine(OSystem *syst, const ADGameDescription *gd) : Freesca
_keysBorderCLUT8 = nullptr;
_menu = nullptr;
_menuButtons = nullptr;
+ _cursorData = nullptr;
+ _crosshairData = nullptr;
+ _cursorW = 0;
+ _cursorH = 0;
_riddleTopFrame = nullptr;
_riddleBottomFrame = nullptr;
@@ -779,6 +783,35 @@ void CastleEngine::pressedKey(const int keycode) {
activate();
}
+void CastleEngine::setAmigaCursor(bool crosshair) {
+ if (!_cursorData || !_crosshairData)
+ return;
+
+ static const byte cursorPalette[16 * 3] = {
+ 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x66, 0x66, 0x66, 0x88, 0x88, 0x88,
+ 0xAA, 0xAA, 0xAA, 0xCC, 0xCC, 0xCC, 0xAA, 0xAA, 0xAA, 0xCC, 0xCC, 0xCC,
+ 0x44, 0x44, 0x44, 0x66, 0x66, 0x66, 0x88, 0x88, 0x88, 0xCC, 0xCC, 0xCC,
+ 0x66, 0x66, 0x66, 0xCC, 0xCC, 0xCC, 0xAA, 0xAA, 0xAA, 0xEE, 0xEE, 0xEE,
+ };
+
+ byte *srcData = crosshair ? _crosshairData : _cursorData;
+ int scale = MAX(1, g_system->getWidth() / _screenW);
+ int sw = _cursorW * scale;
+ int sh = _cursorH * scale;
+ int hotX = crosshair ? 7 * scale : 1 * scale;
+ int hotY = crosshair ? 7 * scale : 1 * scale;
+
+ byte *scaledCursor = new byte[sw * sh];
+ for (int y = 0; y < sh; y++)
+ for (int x = 0; x < sw; x++)
+ scaledCursor[y * sw + x] = srcData[(y / scale) * _cursorW + (x / scale)];
+
+ Graphics::PixelFormat cursorFormat = Graphics::PixelFormat::createFormatCLUT8();
+ CursorMan.replaceCursor(scaledCursor, sw, sh, hotX, hotY, 0, false, &cursorFormat);
+ CursorMan.replaceCursorPalette(cursorPalette, 0, 16);
+ delete[] scaledCursor;
+}
+
void CastleEngine::drawInfoMenu() {
PauseToken pauseToken = pauseEngine();
if (_savedScreen) {
@@ -832,31 +865,81 @@ void CastleEngine::drawInfoMenu() {
}
}
} else if (isAmiga() || isAtariST()) {
+ if (_cursorData)
+ setAmigaCursor(false); // arrow for the menu
+ else
+ CursorMan.setDefaultArrowCursor();
+ CursorMan.showMouse(true);
if (_menu)
surface->copyRectToSurface(*_menu, 47, 35, Common::Rect(0, 0, MIN<int>(_menu->w, surface->w - 47), MIN<int>(_menu->h, surface->h - 35)));
_gfx->readFromPalette(15, r, g, b);
front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b);
- drawStringInSurface(Common::String::format("%07d", score), 166, 71, front, black, surface);
- drawStringInSurface(centerAndPadString(Common::String::format("%s", _messagesList[135 + shield / 6].c_str()), 10), 151, 102, front, black, surface);
- Common::String keysCollected = _messagesList[141];
- Common::replace(keysCollected, "X", Common::String::format("%d", _keysCollected.size()));
- drawStringInSurface(keysCollected, 103, 41, front, black, surface);
+ // Positions and formulas from assembly at FUN_1AE0:
+ // Score at (167, 71): move.w #$a7,d0; move.w #$47,d1
+ drawStringInSurface(Common::String::format("%07d", score), 167, 71, front, black, surface);
+
+ // Shield at (154, 102): move.w #$9a,d0; move.w #$66,d1
+ // Index = (shield - 1) / 4 (from: subq #1,d0; lsr #2,d0; muls #$c,d0)
+ // Amiga shield text at message indices 171-177 (skipping 174 which is empty)
+ {
+ static const int kAmigaShieldMsgIdx[] = {171, 172, 173, 175, 176, 177};
+ int shieldIdx = (shield > 0) ? (shield - 1) / 4 : 0;
+ if (shieldIdx > 5) shieldIdx = 5;
+ drawStringInSurface(centerAndPadString(_messagesList[kAmigaShieldMsgIdx[shieldIdx]], 10), 154, 102, front, black, surface);
+ }
- Common::String spiritsDestroyedString = _messagesList[133];
- Common::replace(spiritsDestroyedString, "X", Common::String::format("%d", spiritsDestroyed));
- drawStringInSurface(spiritsDestroyedString, 145, 132, front, black, surface);
+ // Keys collected at (104, 41): move.w #$68,d0; move.w #$29,d1 (from FUN_22CC)
+ // Messages: 162="NO KEYS COLLECTED", 163="XX KEYS COLLECTED", 164=" 1 KEY COLLECTED"
+ {
+ Common::String keysText;
+ int numKeys = _keysCollected.size();
+ if (numKeys == 0)
+ keysText = _messagesList[162];
+ else if (numKeys == 1)
+ keysText = _messagesList[164];
+ else {
+ keysText = _messagesList[163];
+ Common::replace(keysText, "XX", Common::String::format("%2d", numKeys));
+ }
+ drawStringInSurface(keysText, 104, 41, front, black, surface);
+ }
- for (int i = 0; i < int(_keysCollected.size()); i++) {
- int y = 58 + (i / 2) * 18;
- if (i % 2 == 0) {
- surface->copyRectToSurfaceWithKey(*_keysBorderFrames[i], 58, y, Common::Rect(0, 0, _keysBorderFrames[i]->w, _keysBorderFrames[i]->h), black);
- keyRects.push_back(Common::Rect(58, y, 58 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
- } else {
- surface->copyRectToSurfaceWithKey(*_keysBorderFrames[i], 80, y, Common::Rect(0, 0, _keysBorderFrames[i]->w, _keysBorderFrames[i]->h), black);
- keyRects.push_back(Common::Rect(80, y, 80 + _keysBorderFrames[i]->w / 2, y + _keysBorderFrames[i]->h));
+ // Spirits destroyed at (145, 133): move.w #$91,d0; move.w #$85,d1
+ // Messages: 156="NONE DESTROYED", 157=" XX DESTROYED "
+ {
+ Common::String spiritsText;
+ if (spiritsDestroyed == 0)
+ spiritsText = _messagesList[156];
+ else {
+ spiritsText = _messagesList[157];
+ Common::replace(spiritsText, "XX", Common::String::format("%2d", spiritsDestroyed));
}
+ drawStringInSurface(spiritsText, 145, 133, front, black, surface);
+ }
+
+ // Movement indicator at (96, 118) from assembly at 0x1BE8-0x1BF0
+ {
+ Graphics::ManagedSurface *moveIndicator = nullptr;
+ if (_playerStepIndex == 0)
+ moveIndicator = _menuCrawlIndicator;
+ else if (_playerStepIndex == 1)
+ moveIndicator = _menuWalkIndicator;
+ else
+ moveIndicator = _menuRunIndicator;
+ if (moveIndicator)
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*moveIndicator, 96, 118,
+ Common::Rect(0, 0, moveIndicator->w, moveIndicator->h), black);
+ }
+
+ // Sound indicator at (96, 103) from assembly at 0x1C1A-0x1C22
+ // Frame 3 = sound off, frame 4 = sound on (offset $360, optionally +$120)
+ {
+ Graphics::ManagedSurface *sndIndicator = _menuFxOnIndicator ? _menuFxOnIndicator : _menuFxOffIndicator;
+ if (sndIndicator)
+ surface->copyRectToSurfaceWithKey((const Graphics::Surface)*sndIndicator, 96, 103,
+ Common::Rect(0, 0, sndIndicator->w, sndIndicator->h), black);
}
} else if (isSpectrum() || isCPC()) {
Common::Array<Common::String> lines;
@@ -942,6 +1025,12 @@ void CastleEngine::drawInfoMenu() {
case Common::EVENT_KEYDOWN:
cont = false;
break;
+ case Common::EVENT_MOUSEMOVE:
+ if ((isAmiga() || isAtariST()) && _cursorData && _crosshairData) {
+ Common::Point mp = getNormalizedPosition(event.mouse);
+ setAmigaCursor(_viewArea.contains(mp));
+ }
+ break;
case Common::EVENT_SCREEN_CHANGED:
_gfx->computeScreenViewport();
// TODO: properly refresh screen
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index ba04558213b..67c1106b456 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -48,6 +48,11 @@ public:
Graphics::ManagedSurface *_menuFxOnIndicator;
Graphics::ManagedSurface *_menuFxOffIndicator;
Graphics::ManagedSurface *_menu;
+ byte *_cursorData; // diagonal arrow (outside view area)
+ byte *_crosshairData; // crosshair (# pointer, inside view area)
+ int _cursorW;
+ int _cursorH;
+ void setAmigaCursor(bool crosshair);
void beforeStarting() override;
void initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target) override;
Commit: c6e76087452d01a7e19a07263bcedc79da7362bf
https://github.com/scummvm/scummvm/commit/c6e76087452d01a7e19a07263bcedc79da7362bf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00
Commit Message:
FREESCAPE: added pulsating surface detection and rendering for dark and castle (amiga/atari)
Changed paths:
engines/freescape/area.cpp
engines/freescape/area.h
engines/freescape/freescape.cpp
engines/freescape/games/castle/amiga.cpp
engines/freescape/games/castle/castle.cpp
engines/freescape/games/dark/amiga.cpp
engines/freescape/games/dark/atari.cpp
engines/freescape/gfx.cpp
engines/freescape/gfx.h
engines/freescape/loaders/8bitBinaryLoader.cpp
diff --git a/engines/freescape/area.cpp b/engines/freescape/area.cpp
index ac288f6c159..e25e31cffc0 100644
--- a/engines/freescape/area.cpp
+++ b/engines/freescape/area.cpp
@@ -74,6 +74,7 @@ Area::Area(uint16 areaID_, uint16 areaFlags_, ObjectMap *objectsByID_, ObjectMap
_underFireBackgroundColor = 255;
_inkColor = 255;
_paperColor = 255;
+ _colorCycling = false;
_gasPocketRadius = 0;
diff --git a/engines/freescape/area.h b/engines/freescape/area.h
index 62a0106acfd..30d5406fb55 100644
--- a/engines/freescape/area.h
+++ b/engines/freescape/area.h
@@ -98,6 +98,7 @@ public:
uint8 _inkColor;
uint8 _paperColor;
uint8 _extraColor[4];
+ bool _colorCycling; // Amiga/Atari: bit 14 of area header enables COLOR15 cycling
ColorReMap _colorRemaps;
uint32 _lastTick;
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index ef0e4654650..e9716409aa1 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -473,6 +473,7 @@ void FreescapeEngine::drawBackground() {
}
void FreescapeEngine::drawFrame() {
+ _gfx->updateColorCycling();
int farClipPlane = _farClipPlane;
if (_currentArea->isOutside())
farClipPlane *= 100;
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index 79932cf223a..1d51207ba19 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -175,6 +175,15 @@ void CastleEngine::loadAssetsAmigaDemo() {
loadPalettes(&file, 0x151a6);
+ // COLOR15 cycling table (mem $8B78, file 0x8B94): 14 entries of 12-bit Amiga RGB + 0xFFFF end.
+ // From assembly: interrupt handler at $12BA cycles $DFF19E through this table every 4 frames.
+ file.seek(0x8b94);
+ while (true) {
+ uint16 val = file.readUint16BE();
+ if (val == 0xFFFF) break;
+ _gfx->_colorCyclingTable.push_back(val);
+ }
+
file.seek(0x2be96); // Area 255
_areaMap[255] = load8bitArea(&file, 16);
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 0c6d2495015..05dcae8d4e5 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -612,6 +612,10 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
swapPalette(areaID);
+ // Enable/disable COLOR15 cycling based on per-area flag (Amiga/Atari)
+ if ((isAmiga() || isAtariST()) && _currentArea)
+ _gfx->_colorCyclingTimer = _currentArea->_colorCycling ? 0 : -1;
+
if (isCPC())
updateCPCSpritesPalette();
diff --git a/engines/freescape/games/dark/amiga.cpp b/engines/freescape/games/dark/amiga.cpp
index 738bd3e430a..70c2c53b13f 100644
--- a/engines/freescape/games/dark/amiga.cpp
+++ b/engines/freescape/games/dark/amiga.cpp
@@ -64,6 +64,23 @@ void DarkEngine::loadAssetsAmigaFullGame() {
}
titleSurface->convertToInPlace(_gfx->_texturePixelFormat, pal.data(), pal.size());
_title = titleSurface;
+
+ // Dark Side: COLOR5 palette cycling from assembly interrupt handler at $10E4.
+ // Cycles $DFF18A (COLOR5) every 2 frames through 30 entries.
+ {
+ static const uint16 kDarkSideCyclingTable[] = {
+ 0x000, 0xE6D, 0x600, 0x900, 0xC00, 0xF00, 0xF30, 0xF60,
+ 0xF90, 0xFC0, 0xFF0, 0xAF0, 0x5F0, 0x6F8, 0x7FD, 0x7EF,
+ 0xBDF, 0xDDF, 0xBCF, 0x9BF, 0x7BF, 0x6BF, 0x5AF, 0x4AF,
+ 0x29F, 0x18F, 0x07F, 0x04C, 0x02A, 0x007
+ };
+ for (int i = 0; i < 30; i++)
+ _gfx->_colorCyclingTable.push_back(kDarkSideCyclingTable[i]);
+ }
+ _gfx->_colorCyclingPaletteIndex = 5;
+ _gfx->_colorCyclingSpeed = 1;
+ _gfx->_colorCyclingTimer = 0; // always active
+
file.close();
Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.drk", "0.drk", 798);
@@ -99,26 +116,6 @@ void DarkEngine::loadAssetsAmigaFullGame() {
_fontLoaded = true;
- GeometricObject *obj = nullptr;
- obj = (GeometricObject *)_areaMap[15]->objectWithID(18);
- assert(obj);
- obj->_cyclingColors = true;
-
- obj = (GeometricObject *)_areaMap[15]->objectWithID(26);
- assert(obj);
- obj->_cyclingColors = true;
-
- for (int i = 0; i < 3; i++) {
- int16 id = 227 + i * 6 - 2;
- for (int j = 0; j < 2; j++) {
- //debugC(1, kFreescapeDebugParser, "Restoring object %d to from ECD %d", id, index);
- obj = (GeometricObject *)_areaMap[255]->objectWithID(id);
- assert(obj);
- obj->_cyclingColors = true;
- id--;
- }
- }
-
for (auto &area : _areaMap) {
// Center and pad each area name so we do not have to do it at each frame
area._value->_name = centerAndPadString(area._value->_name, 26);
diff --git a/engines/freescape/games/dark/atari.cpp b/engines/freescape/games/dark/atari.cpp
index f7f52cb525d..8b25e6c266e 100644
--- a/engines/freescape/games/dark/atari.cpp
+++ b/engines/freescape/games/dark/atari.cpp
@@ -30,6 +30,22 @@ void DarkEngine::loadAssetsAtariFullGame() {
Common::File file;
file.open("0.drk");
_title = loadAndConvertNeoImage(&file, 0x13ec);
+
+ // Atari ST Dark Side: same COLOR5 cycling as Amiga.
+ {
+ static const uint16 kDarkSideCyclingTable[] = {
+ 0x000, 0xE6D, 0x600, 0x900, 0xC00, 0xF00, 0xF30, 0xF60,
+ 0xF90, 0xFC0, 0xFF0, 0xAF0, 0x5F0, 0x6F8, 0x7FD, 0x7EF,
+ 0xBDF, 0xDDF, 0xBCF, 0x9BF, 0x7BF, 0x6BF, 0x5AF, 0x4AF,
+ 0x29F, 0x18F, 0x07F, 0x04C, 0x02A, 0x007
+ };
+ for (int i = 0; i < 30; i++)
+ _gfx->_colorCyclingTable.push_back(kDarkSideCyclingTable[i]);
+ }
+ _gfx->_colorCyclingPaletteIndex = 5;
+ _gfx->_colorCyclingSpeed = 1;
+ _gfx->_colorCyclingTimer = 0;
+
file.close();
Common::SeekableReadStream *stream = decryptFileAmigaAtari("1.drk", "0.drk", 840);
@@ -55,26 +71,6 @@ void DarkEngine::loadAssetsAtariFullGame() {
loadGlobalObjects(stream, 0x32f6, 24);
loadSoundsFx(stream, 0x266e8, 11);
- GeometricObject *obj = nullptr;
- obj = (GeometricObject *)_areaMap[15]->objectWithID(18);
- assert(obj);
- obj->_cyclingColors = true;
-
- obj = (GeometricObject *)_areaMap[15]->objectWithID(26);
- assert(obj);
- obj->_cyclingColors = true;
-
- for (int i = 0; i < 3; i++) {
- int16 id = 227 + i * 6 - 2;
- for (int j = 0; j < 2; j++) {
- //debugC(1, kFreescapeDebugParser, "Restoring object %d to from ECD %d", id, index);
- obj = (GeometricObject *)_areaMap[255]->objectWithID(id);
- assert(obj);
- obj->_cyclingColors = true;
- id--;
- }
- }
-
for (auto &area : _areaMap) {
// Center and pad each area name so we do not have to do it at each frame
area._value->_name = centerAndPadString(area._value->_name, 26);
diff --git a/engines/freescape/gfx.cpp b/engines/freescape/gfx.cpp
index 49a4a6fe1c7..0c1df92916c 100644
--- a/engines/freescape/gfx.cpp
+++ b/engines/freescape/gfx.cpp
@@ -48,6 +48,10 @@ Renderer::Renderer(int screenW, int screenH, Common::RenderMode renderMode, bool
_palette = nullptr;
_colorMap = nullptr;
_colorRemaps = nullptr;
+ _colorCyclingIndex = 0;
+ _colorCyclingTimer = -1;
+ _colorCyclingPaletteIndex = 15;
+ _colorCyclingSpeed = 3;
_renderMode = renderMode;
_isAccelerated = false;
_debugRenderBoundingBoxes = false;
@@ -299,11 +303,32 @@ void Renderer::setColorMap(ColorMap *colorMap_) {
}
void Renderer::readFromPalette(uint8 index, uint8 &r, uint8 &g, uint8 &b) {
+ // Amiga/Atari: COLOR15 hardware palette cycling.
+ // From assembly: interrupt handler at $12BA cycles $DFF19E (COLOR15)
+ // through table at $8B78 every 4 frames, gated by per-area flag at $7EC2.
+ // Only palette index 15 is affected; other indices render normally.
+ if (index == _colorCyclingPaletteIndex && _colorCyclingTimer >= 0 && !_colorCyclingTable.empty()) {
+ uint16 val = _colorCyclingTable[_colorCyclingIndex];
+ r = ((val >> 8) & 0xF) * 17;
+ g = ((val >> 4) & 0xF) * 17;
+ b = (val & 0xF) * 17;
+ return;
+ }
r = _palette[3 * index + 0];
g = _palette[3 * index + 1];
b = _palette[3 * index + 2];
}
+void Renderer::updateColorCycling() {
+ if (_colorCyclingTimer < 0 || _colorCyclingTable.empty())
+ return;
+ _colorCyclingTimer--;
+ if (_colorCyclingTimer < 0) {
+ _colorCyclingTimer = _colorCyclingSpeed;
+ _colorCyclingIndex = (_colorCyclingIndex + 1) % _colorCyclingTable.size();
+ }
+}
+
uint8 Renderer::indexFromColor(uint8 r, uint8 g, uint8 b) {
for (int i = 0; i < 16; i++) {
if (r == _palette[3 * i + 0] && g == _palette[3 * i + 1] && b == _palette[3 * i + 2])
@@ -533,12 +558,27 @@ bool Renderer::getRGBAt(uint8 index, uint8 ecolor, uint8 &r1, uint8 &g1, uint8 &
}
if (_renderMode == Common::kRenderAmiga || _renderMode == Common::kRenderAtariST) {
+ // Hardware palette cycling: if the main color index matches the cycling
+ // palette entry and cycling is active, use the cycling color directly.
+ // This must happen BEFORE color pair resolution since on real hardware
+ // the Copper list sets the color register globally.
+ if (index == _colorCyclingPaletteIndex && _colorCyclingTimer >= 0 && !_colorCyclingTable.empty()) {
+ readFromPalette(index, r1, g1, b1);
+ r2 = r1; g2 = g1; b2 = b1;
+ if (ecolor > 0)
+ readFromPalette(ecolor, r2, g2, b2);
+ return true;
+ }
+
if (_colorPair[index] > 0) {
int color = 0;
color = _colorPair[index] & 0xf;
readFromPalette(color, r1, g1, b1);
color = _colorPair[index] >> 4;
readFromPalette(color, r2, g2, b2);
+ // Also apply cycling to ecolor if it matches the cycling index
+ if (ecolor > 0 && ecolor == _colorCyclingPaletteIndex && _colorCyclingTimer >= 0 && !_colorCyclingTable.empty())
+ readFromPalette(ecolor, r2, g2, b2);
return true;
} else if (_colorRemaps && _colorRemaps->contains(index)) {
int color = (*_colorRemaps)[index];
diff --git a/engines/freescape/gfx.h b/engines/freescape/gfx.h
index ba9187c2a3a..1986ea5db35 100644
--- a/engines/freescape/gfx.h
+++ b/engines/freescape/gfx.h
@@ -290,6 +290,16 @@ public:
Common::Point _shakeOffset;
byte _stipples[16][128];
+ // Amiga/Atari hardware palette cycling for pulsating surfaces.
+ // Castle Master: COLOR15, every 4 frames, per-area gated.
+ // Dark Side: COLOR5, every 2 frames, always active.
+ Common::Array<uint16> _colorCyclingTable;
+ int _colorCyclingIndex;
+ int _colorCyclingTimer; // -1 = disabled, >=0 = active
+ int _colorCyclingPaletteIndex; // which palette entry to cycle (5 or 15)
+ int _colorCyclingSpeed; // frames between changes (2 or 4)
+ void updateColorCycling();
+
int _scale;
// debug flags
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index ccd8cbb2215..b033012f6d3 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -621,7 +621,17 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
Common::String name;
uint32 base = file->pos();
debugC(1, kFreescapeDebugParser, "Area base: %x", base);
- uint8 areaFlags = readField(file, 8);
+ // On Amiga/Atari, the first field is 16-bit. Bit 14 (0x4000) enables COLOR15 cycling.
+ // From assembly at $10076: move.w (a1),d0; bclr #$e,d0 â extracts bit 14.
+ bool areaColorCycling = false;
+ uint8 areaFlags;
+ if (isAmiga() || isAtariST()) {
+ uint16 fullWord = file->readUint16BE();
+ areaColorCycling = (fullWord & 0x4000) != 0;
+ areaFlags = fullWord & 0xFF;
+ } else {
+ areaFlags = readField(file, 8);
+ }
uint8 numberOfObjects = readField(file, 8);
uint8 areaNumber = readField(file, 8);
@@ -800,6 +810,7 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
area->_usualBackgroundColor = usualBackgroundColor;
area->_underFireBackgroundColor = underFireBackgroundColor;
+ area->_colorCycling = areaColorCycling;
area->_extraColor[0] = extraColor[0];
area->_extraColor[1] = extraColor[1];
area->_extraColor[2] = extraColor[2];
Commit: e4f5bc30f914feb38e90cc90de0fe2b7f5443b2f
https://github.com/scummvm/scummvm/commit/e4f5bc30f914feb38e90cc90de0fe2b7f5443b2f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-25T09:23:16+01:00
Commit Message:
FREESCAPE: added an option for WASD movement and shift for run
Changed paths:
engines/freescape/detection.cpp
engines/freescape/detection.h
engines/freescape/events.cpp
engines/freescape/freescape.cpp
engines/freescape/freescape.h
engines/freescape/games/castle/castle.cpp
engines/freescape/games/castle/castle.h
engines/freescape/games/eclipse/eclipse.cpp
engines/freescape/metaengine.cpp
engines/freescape/movement.cpp
diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 68a09784e0b..f5ef949d445 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -570,7 +570,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformAmstradCPC,
ADGF_DEMO,
- GUIO2(GUIO_NOMIDI, GUIO_RENDERCPC)
+ GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
},
{
"totaleclipse",
@@ -584,7 +584,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformZX,
ADGF_DEMO | GF_ZX_DEMO_MICROHOBBY,
- GUIO2(GUIO_NOMIDI, GUIO_RENDERZX)
+ GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
},
{
"totaleclipse",
@@ -598,7 +598,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformZX,
ADGF_DEMO | GF_ZX_DEMO_CRASH,
- GUIO2(GUIO_NOMIDI, GUIO_RENDERZX)
+ GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
},
{
"totaleclipse",
@@ -607,7 +607,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformZX,
ADGF_NO_FLAGS,
- GUIO2(GUIO_NOMIDI, GUIO_RENDERZX)
+ GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
},
{
"totaleclipse2",
@@ -616,7 +616,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformZX,
ADGF_NO_FLAGS,
- GUIO2(GUIO_NOMIDI, GUIO_RENDERZX)
+ GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
},
{
"totaleclipse",
@@ -701,7 +701,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO4(GUIO_NOMIDI, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_MODERN_MOVEMENT)
+ GUIO5(GUIO_NOMIDI, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
},
{
// Erbe Software release
@@ -718,7 +718,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO4(GUIO_NOMIDI, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_MODERN_MOVEMENT)
+ GUIO5(GUIO_NOMIDI, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
},
{
"totaleclipse", // Tape relese
@@ -802,7 +802,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformZX,
GF_ZX_RETAIL,
- GUIO3(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX)
+ GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX, GAMEOPTION_WASD_CONTROLS)
},
// Disc release
{
@@ -812,7 +812,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformZX,
GF_ZX_DISC,
- GUIO3(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX)
+ GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX, GAMEOPTION_WASD_CONTROLS)
},
// Spanish release was disc-only?
{
@@ -822,7 +822,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::ES_ESP,
Common::kPlatformZX,
GF_ZX_DISC,
- GUIO3(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX)
+ GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERZX, GAMEOPTION_WASD_CONTROLS)
},
{
"castlemaster",
@@ -836,7 +836,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_DEMO,
- GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+ GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
},
{
"castlemaster",
@@ -849,7 +849,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformAmiga,
ADGF_UNSTABLE | ADGF_DEMO,
- GUIO3(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA)
+ GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA, GAMEOPTION_WASD_CONTROLS)
},
// Stampede Amiga, Issue 1, July 1990
{
@@ -863,7 +863,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformAmiga,
ADGF_UNSTABLE | ADGF_DEMO,
- GUIO3(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA)
+ GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDERAMIGA, GAMEOPTION_WASD_CONTROLS)
},
{
"castlemaster",
@@ -877,7 +877,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformAmstradCPC,
ADGF_UNSTABLE,
- GUIO2(GUIO_NOMIDI, GUIO_RENDERCPC)
+ GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_TRAVEL_ROCK, GAMEOPTION_WASD_CONTROLS)
},
{
"castlemaster",
@@ -893,7 +893,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::UNK_LANG,
Common::kPlatformDOS,
ADGF_PIRATED,
- GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+ GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
},
{
"castlemaster",
@@ -909,7 +909,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::UNK_LANG, // Multi-language
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+ GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
},
{
"castlemaster",
@@ -925,7 +925,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::UNK_LANG, // Multi-language
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+ GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
},
{
"castlemaster",
@@ -941,7 +941,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::UNK_LANG, // Multi-language
Common::kPlatformDOS,
ADGF_UNSUPPORTED, // Compressed with lzexe
- GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+ GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
},
{
"castlemaster",
@@ -957,7 +957,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::UNK_LANG, // Multi-language
Common::kPlatformDOS,
ADGF_UNSUPPORTED, // Game data offset are different
- GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+ GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
},
{
"castlemaster",
@@ -973,7 +973,7 @@ static const ADGameDescription gameDescriptions[] = {
Common::ES_ESP,
Common::kPlatformDOS,
ADGF_UNSTABLE,
- GUIO4(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA)
+ GUIO5(GUIO_NOMIDI, GAMEOPTION_TRAVEL_ROCK, GUIO_RENDEREGA, GUIO_RENDERCGA, GAMEOPTION_WASD_CONTROLS)
},
// 3D Construction Kit games
{
diff --git a/engines/freescape/detection.h b/engines/freescape/detection.h
index 0bbc27bba04..5e0a562c11f 100644
--- a/engines/freescape/detection.h
+++ b/engines/freescape/detection.h
@@ -35,9 +35,10 @@
// Driller options
#define GAMEOPTION_AUTOMATIC_DRILLING GUIO_GAMEOPTIONS8
-// Castle Master options
+// Castle Master / Total Eclipse options
#define GAMEOPTION_TRAVEL_ROCK GUIO_GAMEOPTIONS9
+#define GAMEOPTION_WASD_CONTROLS GUIO_GAMEOPTIONS11
#endif
diff --git a/engines/freescape/events.cpp b/engines/freescape/events.cpp
index db5f0039d51..d47713feef5 100644
--- a/engines/freescape/events.cpp
+++ b/engines/freescape/events.cpp
@@ -42,14 +42,23 @@ bool EventManagerWrapper::pollEvent(Common::Event &event) {
break;
_currentActionDown = event.customType;
_keyRepeatTime = time + kKeyRepeatInitialDelay;
+ // Track all simultaneously held actions
+ if (Common::find(_activeActions.begin(), _activeActions.end(), event.customType) == _activeActions.end())
+ _activeActions.push_back(event.customType);
break;
case Common::EVENT_CUSTOM_ENGINE_ACTION_END:
if (event.customType == kActionEscape)
break;
if (event.customType == _currentActionDown) {
- // Only stop firing events if it's the current key
_currentActionDown = kActionNone;
}
+ // Remove from active actions
+ for (uint i = 0; i < _activeActions.size(); i++) {
+ if (_activeActions[i] == event.customType) {
+ _activeActions.remove_at(i);
+ break;
+ }
+ }
break;
case Common::EVENT_KEYDOWN:
if (event.kbd == Common::KEYCODE_ESCAPE || event.kbd == Common::KEYCODE_F5)
@@ -102,6 +111,7 @@ void EventManagerWrapper::purgeKeyboardEvents() {
_delegate->purgeKeyboardEvents();
_currentKeyDown.reset();
_currentActionDown = kActionNone;
+ _activeActions.clear();
_keyRepeatTime = 0;
}
@@ -120,7 +130,7 @@ void EventManagerWrapper::clearExitEvents() {
}
bool EventManagerWrapper::isActionActive(const Common::CustomEventType &action) {
- return _currentActionDown == action;
+ return Common::find(_activeActions.begin(), _activeActions.end(), action) != _activeActions.end();
}
bool EventManagerWrapper::isKeyPressed() {
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index e9716409aa1..d8ee03c26bc 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -131,6 +131,10 @@ FreescapeEngine::FreescapeEngine(OSystem *syst, const ADGameDescription *gd)
if (!Common::parseBool(ConfMan.get("smooth_movement"), _smoothMovement))
error("Failed to parse bool from smooth_movement option");
+ _useWASDControls = false;
+ if (ConfMan.hasKey("wasd_controls"))
+ Common::parseBool(ConfMan.get("wasd_controls"), _useWASDControls);
+
if (isDriller() || isSpaceStationOblivion() || isDark())
_smoothMovement = false;
@@ -181,6 +185,7 @@ FreescapeEngine::FreescapeEngine(OSystem *syst, const ADGameDescription *gd)
// TODO: this is not the same for every game
_playerStepIndex = 6;
+ _savedPlayerStepIndex = -1;
_playerSteps.push_back(1);
_playerSteps.push_back(2);
_playerSteps.push_back(5);
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 12c3788a2ed..6f191661109 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -104,6 +104,7 @@ enum FreescapeAction {
kActionRunMode,
kActionWalkMode,
kActionCrawlMode,
+ kActionRunModifier, // Shift-to-run: held = run, released = walk
kActionSelectPrince,
kActionSelectPrincess,
kActionQuit,
@@ -176,7 +177,8 @@ private:
Common::EventManager *_delegate;
Common::KeyState _currentKeyDown;
- Common::CustomEventType _currentActionDown;
+ Common::CustomEventType _currentActionDown; // last action for key repeat
+ Common::Array<Common::CustomEventType> _activeActions; // all currently held actions
uint32 _keyRepeatTime;
};
@@ -351,6 +353,7 @@ public:
bool _invertY;
bool _smoothMovement;
+ bool _useWASDControls;
// Player movement state
bool _moveForward;
bool _moveBackward;
@@ -429,6 +432,7 @@ public:
uint16 _stepUpDistance;
int _playerStepIndex;
+ int _savedPlayerStepIndex; // saved before shift-to-run, restored on release
Common::Array<int> _playerSteps;
Common::Point crossairPosToMousePos(const Common::Point &crossairPos);
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 05dcae8d4e5..c28eea204bc 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -521,7 +521,7 @@ void CastleEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *inf
act = new Common::Action("WALK", _("Walk"));
act->setCustomEngineActionEvent(kActionWalkMode);
act->addDefaultInputMapping("JOY_B");
- act->addDefaultInputMapping("w");
+ act->addDefaultInputMapping(_useWASDControls ? "2" : "w");
engineKeyMap->addAction(act);
act = new Common::Action("CRAWL", _("Crawl"));
@@ -537,8 +537,17 @@ void CastleEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *inf
act = new Common::Action("ACTIVATE", _("Activate"));
act->setCustomEngineActionEvent(kActionActivate);
- act->addDefaultInputMapping("a");
+ act->addDefaultInputMapping(_useWASDControls ? "e" : "a");
engineKeyMap->addAction(act);
+
+ if (_useWASDControls) {
+ act = new Common::Action("RUNMOD", _("Run (hold)"));
+ act->setCustomEngineActionEvent(kActionRunModifier);
+ act->addDefaultInputMapping("LSHIFT");
+ act->addDefaultInputMapping("RSHIFT");
+ act->addDefaultInputMapping("JOY_LEFT_TRIGGER");
+ engineKeyMap->addAction(act);
+ }
}
void CastleEngine::beforeStarting() {
@@ -780,6 +789,20 @@ void CastleEngine::pressedKey(const int keycode) {
}
_playerStepIndex = 0;
insertTemporaryMessage(_messagesList[13], _countdown - 2);
+ } else if (keycode == kActionRunModifier) {
+ // Shift-to-run: save current mode, switch to run while held
+ if (_playerStepIndex == 2)
+ return; // already running
+ if (_playerHeightNumber == 0) {
+ if (_gameStateVars[k8bitVariableShield] <= 3)
+ return;
+ if (!rise()) {
+ return;
+ }
+ _gameStateVars[k8bitVariableCrawling] = 0;
+ }
+ _savedPlayerStepIndex = _playerStepIndex;
+ _playerStepIndex = 2;
} else if (keycode == kActionFaceForward) {
_pitch = 0;
updateCamera();
@@ -787,6 +810,16 @@ void CastleEngine::pressedKey(const int keycode) {
activate();
}
+void CastleEngine::releasedKey(const int keycode) {
+ if (keycode == kActionRunModifier) {
+ // Shift released: restore the mode from before running
+ if (_savedPlayerStepIndex >= 0) {
+ _playerStepIndex = _savedPlayerStepIndex;
+ _savedPlayerStepIndex = -1;
+ }
+ }
+}
+
void CastleEngine::setAmigaCursor(bool crosshair) {
if (!_cursorData || !_crosshairData)
return;
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 67c1106b456..207c0ec9b5f 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -82,6 +82,7 @@ public:
void drawLiftingGate(Graphics::Surface *surface);
void drawDroppingGate(Graphics::Surface *surface);
void pressedKey(const int keycode) override;
+ void releasedKey(const int keycode) override;
void checkSensors() override;
void updateTimeVariables() override;
void drawBackground() override;
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index b3bce7135f3..bf093963bbd 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -263,21 +263,30 @@ void EclipseEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *in
act = new Common::Action("ROTR", _("Rotate right"));
act->setCustomEngineActionEvent(kActionRotateRight);
- act->addDefaultInputMapping("w");
+ act->addDefaultInputMapping(_useWASDControls ? "e" : "w");
engineKeyMap->addAction(act);
// I18N: Illustrates the angle at which you turn left or right.
act = new Common::Action("CHNGANGLE", _("Change angle"));
act->setCustomEngineActionEvent(kActionIncreaseAngle);
- act->addDefaultInputMapping("a");
+ act->addDefaultInputMapping(_useWASDControls ? "v" : "a");
engineKeyMap->addAction(act);
// I18N: STEP SIZE: Measures the size of one movement in the direction you are facing (1-250 standard distance units (SDUs))
act = new Common::Action("CHNGSTEPSIZE", _("Change step size"));
act->setCustomEngineActionEvent(kActionChangeStepSize);
- act->addDefaultInputMapping("s");
+ act->addDefaultInputMapping(_useWASDControls ? "x" : "s");
engineKeyMap->addAction(act);
+ if (_useWASDControls) {
+ act = new Common::Action("RUNMOD", _("Sprint (hold)"));
+ act->setCustomEngineActionEvent(kActionRunModifier);
+ act->addDefaultInputMapping("LSHIFT");
+ act->addDefaultInputMapping("RSHIFT");
+ act->addDefaultInputMapping("JOY_LEFT_TRIGGER");
+ engineKeyMap->addAction(act);
+ }
+
act = new Common::Action("TGGLHEIGHT", _("Toggle height"));
act->setCustomEngineActionEvent(kActionToggleRiseLower);
act->addDefaultInputMapping("JOY_B");
@@ -540,6 +549,12 @@ void EclipseEngine::pressedKey(const int keycode) {
updateCamera();
} else if (keycode == kActionToggleFlashlight) {
_flashlightOn = !_flashlightOn;
+ } else if (keycode == kActionRunModifier) {
+ // Shift-to-sprint: save current step, switch to max while held
+ if (_savedPlayerStepIndex < 0) {
+ _savedPlayerStepIndex = _playerStepIndex;
+ _playerStepIndex = (int)_playerSteps.size() - 1;
+ }
}
}
@@ -619,6 +634,13 @@ bool EclipseEngine::onScreenControls(Common::Point mouse) {
void EclipseEngine::releasedKey(const int keycode) {
if (keycode == kActionRiseOrFlyUp)
_resting = false;
+ else if (keycode == kActionRunModifier) {
+ // Shift released: restore previous step size
+ if (_savedPlayerStepIndex >= 0) {
+ _playerStepIndex = _savedPlayerStepIndex;
+ _savedPlayerStepIndex = -1;
+ }
+ }
}
void EclipseEngine::drawAnalogClock(Graphics::Surface *surface, int x, int y, uint32 colorHand1, uint32 colorHand2, uint32 colorBack) {
diff --git a/engines/freescape/metaengine.cpp b/engines/freescape/metaengine.cpp
index 198c3f87927..4b9bb8685d5 100644
--- a/engines/freescape/metaengine.cpp
+++ b/engines/freescape/metaengine.cpp
@@ -146,6 +146,18 @@ static const ADExtraGuiOptionsMap optionsList[] = {
0
}
},
+ {
+ GAMEOPTION_WASD_CONTROLS,
+ {
+ // I18N: Use modern FPS-style controls: WASD for movement, Shift to run
+ _s("WASD controls"),
+ _s("Use WASD keys for movement and Shift to run"),
+ "wasd_controls",
+ false,
+ 0,
+ 0
+ }
+ },
AD_EXTRA_GUI_OPTIONS_TERMINATOR
};
diff --git a/engines/freescape/movement.cpp b/engines/freescape/movement.cpp
index 56b47946853..f61677c5e6c 100644
--- a/engines/freescape/movement.cpp
+++ b/engines/freescape/movement.cpp
@@ -37,6 +37,8 @@ void FreescapeEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *
act->addDefaultInputMapping("UP");
act->addDefaultInputMapping("JOY_UP");
act->addDefaultInputMapping("o");
+ if (_useWASDControls)
+ act->addDefaultInputMapping("w");
engineKeyMap->addAction(act);
act = new Common::Action(Common::kStandardActionMoveDown, _("Down"));
@@ -44,20 +46,24 @@ void FreescapeEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *
act->addDefaultInputMapping("DOWN");
act->addDefaultInputMapping("JOY_DOWN");
act->addDefaultInputMapping("k");
+ if (_useWASDControls)
+ act->addDefaultInputMapping("s");
engineKeyMap->addAction(act);
act = new Common::Action(Common::kStandardActionMoveLeft, _("Strafe left"));
act->setCustomEngineActionEvent(kActionMoveLeft);
act->addDefaultInputMapping("LEFT");
act->addDefaultInputMapping("JOY_LEFT");
- // act->addDefaultInputMapping("q");
+ if (_useWASDControls)
+ act->addDefaultInputMapping("a");
engineKeyMap->addAction(act);
act = new Common::Action(Common::kStandardActionMoveRight, _("Strafe right"));
act->setCustomEngineActionEvent(kActionMoveRight);
act->addDefaultInputMapping("RIGHT");
act->addDefaultInputMapping("JOY_RIGHT");
- // act->addDefaultInputMapping("w");
+ if (_useWASDControls)
+ act->addDefaultInputMapping("d");
engineKeyMap->addAction(act);
act = new Common::Action("SHOOT", _("Shoot"));
@@ -448,13 +454,10 @@ void FreescapeEngine::updatePlayerMovementClassic(float deltaTime) {
void FreescapeEngine::updatePlayerMovementSmooth(float deltaTime) {
if (_moveForward && !_eventManager->isActionActive(kActionMoveUp))
_moveForward = false;
-
if (_moveBackward && !_eventManager->isActionActive(kActionMoveDown))
_moveBackward = false;
-
if (_strafeLeft && !_eventManager->isActionActive(kActionMoveLeft))
_strafeLeft = false;
-
if (_strafeRight && !_eventManager->isActionActive(kActionMoveRight))
_strafeRight = false;
More information about the Scummvm-git-logs
mailing list