[Scummvm-git-logs] scummvm master -> dc7ddeb4bdcd7e54ad6c2778e08012fdfdf76aa2
npjg
noreply at scummvm.org
Sun Jul 19 01:01:25 UTC 2026
This automated email contains information about 13 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
5a6d6f450f MEDIASTATION: Move Lion King constellations minigame to its own folder
b12ec7e8b4 MEDIASTATION: Implement mouse position update methods
afff90261d MEDIASTATION: Implement hardcoded MoveSophie function for Puzzle Castle
64a9d13066 MEDIASTATION: Warn on duplicate script responses
384719a0ed MEDIASTATION: Implement weird Puzzle Castle override for transitions
f3ed45542d MEDIASTATION: Fix incorrect dissolve factor massaging
3ab53f3f68 MEDIASTATION: Draw uncompressed bitmaps by their width, not stride
d44b033f89 MEDIASTATION: Stub out printer actor
0fcbd2cc2c MEDIASTATION: Get text input working for Puzzle Castle
e608521bed MEDIASTATION: Implement key events on hotspots
0591485c99 MEDIASTATION: Implement hardcoded stalking minigame for Lion King
edb7728208 MEDIASTATION: Implement hardcoded Dalmatians maze minigame
dc7ddeb4bd MEDIASTATION: Implement hardcoded checkers minigame for Hercules
Commit: 5a6d6f450f4fc7b33716f640a39700b6f13009eb
https://github.com/scummvm/scummvm/commit/5a6d6f450f4fc7b33716f640a39700b6f13009eb
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:55:59-04:00
Commit Message:
MEDIASTATION: Move Lion King constellations minigame to its own folder
Changed paths:
A engines/mediastation/minigames/dotgame.cpp
A engines/mediastation/minigames/dotgame.h
R engines/mediastation/actors/dotgame.cpp
R engines/mediastation/actors/dotgame.h
engines/mediastation/context.cpp
engines/mediastation/module.mk
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index bb6f7be1994..a9b584617df 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -30,7 +30,6 @@
#include "mediastation/actors/canvas.h"
#include "mediastation/actors/cursor.h"
#include "mediastation/actors/diskimage.h"
-#include "mediastation/actors/dotgame.h"
#include "mediastation/actors/palette.h"
#include "mediastation/actors/image.h"
#include "mediastation/actors/path.h"
@@ -43,6 +42,7 @@
#include "mediastation/actors/screen.h"
#include "mediastation/actors/font.h"
#include "mediastation/actors/text.h"
+#include "mediastation/minigames/dotgame.h"
namespace MediaStation {
diff --git a/engines/mediastation/actors/dotgame.cpp b/engines/mediastation/minigames/dotgame.cpp
similarity index 99%
rename from engines/mediastation/actors/dotgame.cpp
rename to engines/mediastation/minigames/dotgame.cpp
index d27e709efc5..98aaa285491 100644
--- a/engines/mediastation/actors/dotgame.cpp
+++ b/engines/mediastation/minigames/dotgame.cpp
@@ -19,13 +19,13 @@
*
*/
-#include "mediastation/actors/dotgame.h"
+#include "graphics/paletteman.h"
+
+#include "mediastation/minigames/dotgame.h"
#include "mediastation/datafile.h"
#include "mediastation/debugchannels.h"
#include "mediastation/mediastation.h"
-#include "graphics/paletteman.h"
-
namespace MediaStation {
void DotGameActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
diff --git a/engines/mediastation/actors/dotgame.h b/engines/mediastation/minigames/dotgame.h
similarity index 95%
rename from engines/mediastation/actors/dotgame.h
rename to engines/mediastation/minigames/dotgame.h
index 53a6a5b49c3..89716f426b4 100644
--- a/engines/mediastation/actors/dotgame.h
+++ b/engines/mediastation/minigames/dotgame.h
@@ -19,8 +19,8 @@
*
*/
-#ifndef MEDIASTATION_DOTGAME_H
-#define MEDIASTATION_DOTGAME_H
+#ifndef MEDIASTATION_MINIGAMES_DOTGAME_H
+#define MEDIASTATION_MINIGAMES_DOTGAME_H
#include "mediastation/actors/stage.h"
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 5761a2fcbf6..2dc80d3af4f 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -7,7 +7,6 @@ MODULE_OBJS = \
actors/cursor.o \
actors/diskimage.o \
actors/document.o \
- actors/dotgame.o \
actors/font.o \
actors/hotspot.o \
actors/image.o \
@@ -38,6 +37,7 @@ MODULE_OBJS = \
mediascript/scriptvalue.o \
mediastation.o \
metaengine.o \
+ minigames/dotgame.o \
profile.o
# This module can be built as a plugin
Commit: b12ec7e8b4e6c1a88a65b9164ad3e3cbffbf978e
https://github.com/scummvm/scummvm/commit/b12ec7e8b4e6c1a88a65b9164ad3e3cbffbf978e
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:55:59-04:00
Commit Message:
MEDIASTATION: Implement mouse position update methods
Changed paths:
engines/mediastation/actors/stage.cpp
engines/mediastation/events.cpp
engines/mediastation/mediastation.h
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index 3b10b0f28b5..274a28b76fa 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -783,7 +783,7 @@ void RootStage::deleteChildrenFromContextId(uint contextId) {
void RootStage::setMousePosition(int16 x, int16 y) {
x += _boundingBox.left;
y += _boundingBox.top;
- warning("[%s] %s: STUB: (%d, %d)", debugName(), __func__, x, y);
+ g_system->warpMouse(x, y);
}
StageDirector::StageDirector() {
diff --git a/engines/mediastation/events.cpp b/engines/mediastation/events.cpp
index 1a3ff6b403c..d5305ebd5ee 100644
--- a/engines/mediastation/events.cpp
+++ b/engines/mediastation/events.cpp
@@ -238,6 +238,15 @@ void MediaStationEngine::dispatchOneSystemEvent(const Common::Event &event) {
}
}
+void MediaStationEngine::generateMouseUpdateEvent() {
+ queueMouseEvent(kMouseEnterExitEvent, g_system->getEventManager()->getMousePos());
+}
+
+void MediaStationEngine::queueMouseEvent(EventType type, const Common::Point &point) {
+ MouseEvent mouseEvent(type, point);
+ _eventLoop->queueEvent(mouseEvent);
+}
+
PreDisplaySyncState EventLoop::preDisplaySync() {
PreDisplaySyncState state = kPreDisplaySyncNoScreenUpdateRequested;
for (auto it = _preDisplaySyncClients.begin(); it != _preDisplaySyncClients.end(); ++it) {
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index f3fd80c5091..f5590256eed 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -87,6 +87,7 @@ public:
const char *getAppName() const;
bool hasFeature(EngineFeature f) const override;
void dispatchOneSystemEvent(const Common::Event &event);
+ void generateMouseUpdateEvent();
VideoDisplayManager *getDisplayManager() { return _displayManager; }
DisplayUpdateManager *getDisplayUpdateManager() { return _displayUpdateManager; }
Commit: afff90261d84f73234212a0e6888345da92c7385
https://github.com/scummvm/scummvm/commit/afff90261d84f73234212a0e6888345da92c7385
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:55:59-04:00
Commit Message:
MEDIASTATION: Implement hardcoded MoveSophie function for Puzzle Castle
Assisted-by: gpt-5.5 claude-sonnet-4.5
Changed paths:
A engines/mediastation/minigames/sophie.cpp
engines/mediastation/mediascript/function.cpp
engines/mediastation/mediascript/function.h
engines/mediastation/mediascript/scriptconstants.h
engines/mediastation/module.mk
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index 8fe2133f552..df491de1b85 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -253,6 +253,11 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
script_Checkers(args, returnValue);
break;
+ case kMoveSophieFunction:
+ FUNCARGCHECK(14);
+ script_MoveSophie(args, returnValue);
+ break;
+
case kLegacy_DebugPrintFunction:
// We don't need to check arg counts here. This just prints however many args we have.
script_DebugPrint(args, returnValue);
diff --git a/engines/mediastation/mediascript/function.h b/engines/mediastation/mediascript/function.h
index 1da34af9610..adf2c16127b 100644
--- a/engines/mediastation/mediascript/function.h
+++ b/engines/mediastation/mediascript/function.h
@@ -100,6 +100,9 @@ private:
// IBM/Crayola.
void script_Drawing(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+
+ // Puzzle Castle.
+ void script_MoveSophie(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
};
} // End of namespace MediaStation
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 146ef5f1b32..7fcc8d35de7 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -97,6 +97,7 @@ enum BuiltInFunction {
kEndTimedIntervalFunction = 0x23,
kCheckersFunction = 0x24, // Hercules
kDrawingFunction = 0x25, // IBM/Crayola
+ kMoveSophieFunction = 0x44c, // Puzzle Castle
// Early engine versions (like for Lion King and such), had different opcodes
// for some functions, even though the functions were the same. So those are
diff --git a/engines/mediastation/minigames/sophie.cpp b/engines/mediastation/minigames/sophie.cpp
new file mode 100644
index 00000000000..8a0a1aa1cc0
--- /dev/null
+++ b/engines/mediastation/minigames/sophie.cpp
@@ -0,0 +1,87 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mediastation/mediascript/function.h"
+#include "mediastation/actors/sprite.h"
+#include "mediastation/mediastation.h"
+#include "math/angle.h"
+
+namespace MediaStation {
+
+// This is a hardcoded function in Puzzle Castle.
+void FunctionManager::script_MoveSophie(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+ // This seems to be hardcoded rather than implemented as a script function due to the need for atan2l.
+ uint sophieActorId = args[0].asActorId();
+ SpriteMovieActor *spriteActor = static_cast<SpriteMovieActor *>(g_engine->getImtGod()->getActorByIdAndType(sophieActorId, kActorTypeSprite));
+
+ // This seems to be some sort of angle perhaps, but it is not used.
+ // int unk1 = static_cast<int>(args[1].asFloat());
+ int dx = static_cast<int>(args[2].asFloat());
+ int dy = static_cast<int>(args[3].asFloat());
+ // args[4] through args[11] are the sprite clip ids. They typically should be:
+ // args[4] = $sophieWalk_North (10018),
+ // args[5] = $sophieWalk_NorthEast (10021),
+ // args[6] = $sophieWalk_East (10020),
+ // args[7] = $sophieWalk_SouthEast (10022),
+ // args[8] = $sophieWalk_South (10019),
+ // args[9] = $sophieWalk_SouthWest (10025),
+ // args[10] = $sophieWalk_West (10023),
+ // args[11] = $sophieWalk_NorthWest (10024)
+ double sophieSpatialCenterX = args[12].asFloat();
+ double sophieSpatialCenterY = args[13].asFloat();
+
+ // Convert the direction we're moving into an octant to select the proper sprite to show.
+ const float angleDegrees = Math::Angle::arcTangent2(dy, dx).getDegrees(0);
+ int octant = int((angleDegrees + 22.5f) / 45.0f) & 7;
+
+ // Get that sprite clip.
+ Common::Array<ScriptValue> sophieArgs;
+ ScriptValue temp;
+ static const int octantToScriptArgIndex[] = {
+ 6, // East
+ 5, // NE
+ 4, // North
+ 11, // NW
+ 10, // West
+ 9, // SW
+ 8, // South
+ 7 // SE
+ };
+ int directionIndex = octantToScriptArgIndex[octant];
+ sophieArgs.push_back(args[directionIndex]);
+ spriteActor->callMethod(kSetCurrentClipMethod, sophieArgs);
+
+ // Move Sophie to the right location.
+ sophieArgs.clear();
+ temp.setToFloat(sophieSpatialCenterX);
+ sophieArgs.push_back(temp);
+ temp.setToFloat(sophieSpatialCenterY);
+ sophieArgs.push_back(temp);
+ spriteActor->callMethod(kSpatialCenterMoveToMethod, sophieArgs);
+
+ // Increment the frame to show Sophie walking.
+ sophieArgs.clear();
+ temp.setToBool(true);
+ sophieArgs.push_back(temp);
+ spriteActor->callMethod(kIncrementFrameMethod, sophieArgs);
+}
+
+} // End of namespace Mediastation
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 2dc80d3af4f..102946cb517 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -38,6 +38,7 @@ MODULE_OBJS = \
mediastation.o \
metaengine.o \
minigames/dotgame.o \
+ minigames/sophie.o \
profile.o
# This module can be built as a plugin
Commit: 64a9d13066a8c58c813b85eead4fae594ef07f12
https://github.com/scummvm/scummvm/commit/64a9d13066a8c58c813b85eead4fae594ef07f12
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:55:59-04:00
Commit Message:
MEDIASTATION: Warn on duplicate script responses
Not throwing an error is required for the Puzzle Castle demo to run.
Changed paths:
engines/mediastation/actor.cpp
diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index 534570433d2..d46e7210071 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -167,7 +167,7 @@ void Actor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
// This is not a hashmap because we don't want to have to hash ScriptValues.
for (ScriptResponse *existingScriptResponse : scriptResponsesForType) {
if (existingScriptResponse->_argumentValue == scriptResponse->_argumentValue) {
- error("[%s] %s: Script response for %s (%s) already exists", debugName(), __func__,
+ warning("[%s] %s: Script response for %s (%s) already exists", debugName(), __func__,
eventTypeToStr(scriptResponse->_type), scriptResponse->_argumentValue.getDebugString().c_str());
}
}
Commit: 384719a0edee6bfa53f33924cb99faddf42d3dba
https://github.com/scummvm/scummvm/commit/384719a0edee6bfa53f33924cb99faddf42d3dba
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:55:59-04:00
Commit Message:
MEDIASTATION: Implement weird Puzzle Castle override for transitions
Changed paths:
engines/mediastation/mediascript/function.cpp
engines/mediastation/mediascript/function.h
engines/mediastation/mediastation.cpp
engines/mediastation/mediastation.h
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index df491de1b85..fac73f8f6c3 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -145,7 +145,7 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
case kEffectTransitionFunction:
case kLegacy_EffectTransitionFunction:
- g_engine->getDisplayManager()->effectTransition(args);
+ script_EffectTransition(args, returnValue);
break;
case kEffectTransitionOnSyncFunction:
@@ -371,13 +371,29 @@ void FunctionManager::script_Random(Common::Array<ScriptValue> &args, ScriptValu
}
void FunctionManager::script_TimeOfDay(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
- TimeDate timeDate;
// Calculate seconds since midnight.
- g_system->getTimeAndDate(timeDate);
- uint32 secondsSinceMidnight = (timeDate.tm_hour * 60 + timeDate.tm_min) * 60 + timeDate.tm_sec;
+ uint secondsSinceMidnight = g_engine->currentTimeInSeconds();
returnValue.setToTime(static_cast<double>(secondsSinceMidnight));
}
+void FunctionManager::script_EffectTransition(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+ // Puzzle Castle has this weird code path where it checks for this magic value.
+ // If so, it doesn't actually do the transition. Otherwise, we just do a normal
+ // transition. Scripts rely on this behavior, so we must reimplement it.
+ bool triggerPuzzleCastleIterationUpdate = (args.size() == 2) && (args[0].asFloat() == 9999);
+ if (triggerPuzzleCastleIterationUpdate) {
+ uint iterations = static_cast<uint>(args[1].asFloat());
+ uint start = g_engine->currentTimeInSeconds();
+ for (unsigned i = 0; i < iterations; ++i) {
+ g_engine->getDisplayUpdateManager()->performUpdateAll();
+ }
+
+ returnValue.setToFloat(g_engine->currentTimeInSeconds() - start);
+ } else {
+ g_engine->getDisplayManager()->effectTransition(args);
+ }
+}
+
void FunctionManager::script_SquareRoot(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
if (args[0].getType() != kScriptValueTypeFloat) {
error("%s: Numeric value required", __func__);
diff --git a/engines/mediastation/mediascript/function.h b/engines/mediastation/mediascript/function.h
index adf2c16127b..fc7432870e8 100644
--- a/engines/mediastation/mediascript/function.h
+++ b/engines/mediastation/mediascript/function.h
@@ -87,6 +87,7 @@ private:
void script_GetRegistry(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
void script_SetProfile(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
void script_DebugPrint(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+ void script_EffectTransition(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
// 101 Dalmatians.
void script_MazeGenerate(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 8b2bcb94e69..7e510b8fb46 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -142,6 +142,14 @@ bool MediaStationEngine::hasFeature(EngineFeature f) const {
return (f == kSupportsReturnToLauncher);
}
+uint MediaStationEngine::currentTimeInSeconds() {
+ // Calculate seconds since midnight.
+ TimeDate timeDate;
+ g_system->getTimeAndDate(timeDate);
+ uint secondsSinceMidnight = (timeDate.tm_hour * 60 + timeDate.tm_min) * 60 + timeDate.tm_sec;
+ return secondsSinceMidnight;
+}
+
Common::Error MediaStationEngine::run() {
_debugger = new Debugger(this);
setDebugger(_debugger);
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index f5590256eed..03035f50e0f 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -88,6 +88,7 @@ public:
bool hasFeature(EngineFeature f) const override;
void dispatchOneSystemEvent(const Common::Event &event);
void generateMouseUpdateEvent();
+ uint currentTimeInSeconds();
VideoDisplayManager *getDisplayManager() { return _displayManager; }
DisplayUpdateManager *getDisplayUpdateManager() { return _displayUpdateManager; }
Commit: f3ed45542d0e74b3d56cc03bfcc2c030374ee944
https://github.com/scummvm/scummvm/commit/f3ed45542d0e74b3d56cc03bfcc2c030374ee944
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:55:59-04:00
Commit Message:
MEDIASTATION: Fix incorrect dissolve factor massaging
Changed paths:
engines/mediastation/graphics.cpp
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index 1b6cff89829..c8594d3f58b 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -884,7 +884,9 @@ void VideoDisplayManager::imageBlit(
if (dissolveFactor > 1.0 || dissolveFactor < 0.0) {
warning("%s: Got out-of-range dissolve factor: %f", __func__, dissolveFactor);
dissolveFactor = CLIP(dissolveFactor, 0.0, 1.0);
- } else if (dissolveFactor == 0.0) {
+ }
+
+ if (dissolveFactor == 0.0) {
// If the image is fully transparent, there is nothing to draw, so we can return now.
return;
} else if (dissolveFactor != 1.0) {
@@ -1042,11 +1044,11 @@ void VideoDisplayManager::dissolveBlitRectsClip(
const Common::Array<Common::Rect> &dirtyRegion,
const uint integralDissolveFactor) {
- byte dissolveIndex = DISSOLVE_PATTERN_COUNT;
+ byte dissolveIndex = DISSOLVE_PATTERN_COUNT - 1;
if (integralDissolveFactor != 50) {
- dissolveIndex = ((integralDissolveFactor + 2) / 4) - 1;
+ dissolveIndex = ((integralDissolveFactor + 2) / 4);
+ dissolveIndex = CLIP<byte>(dissolveIndex, 0, (DISSOLVE_PATTERN_COUNT - 2));
}
- dissolveIndex = CLIP<byte>(dissolveIndex, 0, (DISSOLVE_PATTERN_COUNT - 1));
Common::Rect destRect(Common::Rect(destPos, source->width(), source->height()));
for (const Common::Rect &dirtyRect : dirtyRegion) {
Commit: 3ab53f3f685b17488cf7e6692f3beb2ba7514837
https://github.com/scummvm/scummvm/commit/3ab53f3f685b17488cf7e6692f3beb2ba7514837
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:55:59-04:00
Commit Message:
MEDIASTATION: Draw uncompressed bitmaps by their width, not stride
In the vast majority of cases, this caused no problems. However,
there were a few sprites in Puzzle Castle that drew garbage on
their edges when drawing with stride.
Changed paths:
engines/mediastation/graphics.cpp
engines/mediastation/graphics.h
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index c8594d3f58b..ee127147341 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -923,7 +923,7 @@ void VideoDisplayManager::imageBlit(
// non-transparent blitting, but we will just use simpleBlitFrom in both
// cases. It will pick the better method if there is no transparent
// color set.
- blitRectsClip(targetImage, destinationPoint, sourceImage->_image, dirtyRegion, useTransBlit);
+ blitRectsClip(targetImage, destinationPoint, sourceImage, dirtyRegion, useTransBlit);
break;
case kRle8Blit | kClipEnabled:
@@ -956,12 +956,12 @@ void VideoDisplayManager::imageBlit(
void VideoDisplayManager::blitRectsClip(
Graphics::ManagedSurface *dest,
const Common::Point &destLocation,
- const Graphics::ManagedSurface &source,
+ const PixMapImage *source,
const Common::Array<Common::Rect> &dirtyRegion,
bool useTransBlit) {
for (const Common::Rect &dirtyRect : dirtyRegion) {
- Common::Rect destRect(destLocation, source.w, source.h);
+ Common::Rect destRect(destLocation, source->width(), source->height());
Common::Rect areaToRedraw = dirtyRect.findIntersectingRect(destRect);
if (!areaToRedraw.isEmpty()) {
@@ -969,9 +969,9 @@ void VideoDisplayManager::blitRectsClip(
Common::Point originOnScreen(areaToRedraw.origin());
areaToRedraw.translate(-destLocation.x, -destLocation.y);
if (useTransBlit) {
- dest->transBlitFrom(source, areaToRedraw, originOnScreen);
+ dest->transBlitFrom(source->_image, areaToRedraw, originOnScreen);
} else {
- dest->simpleBlitFrom(source, areaToRedraw, originOnScreen);
+ dest->simpleBlitFrom(source->_image, areaToRedraw, originOnScreen);
}
}
}
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index c9cefa80b3f..844600c3411 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -243,7 +243,7 @@ private:
void blitRectsClip(
Graphics::ManagedSurface *dest,
const Common::Point &destLocation,
- const Graphics::ManagedSurface &source,
+ const PixMapImage *source,
const Common::Array<Common::Rect> &dirtyRegion,
bool useTransBlit = false);
void rleBlitRectsClip(
Commit: d44b033f893b4399b3db41f8c03831deacc47d43
https://github.com/scummvm/scummvm/commit/d44b033f893b4399b3db41f8c03831deacc47d43
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:55:59-04:00
Commit Message:
MEDIASTATION: Stub out printer actor
Having a printer actor is necessary to get to the last screen in Puzzle Castle,
even if nothing is printed from there.
Changed paths:
A engines/mediastation/actors/printer.cpp
A engines/mediastation/actors/printer.h
engines/mediastation/context.cpp
engines/mediastation/graphics.cpp
engines/mediastation/graphics.h
engines/mediastation/mediascript/scriptconstants.cpp
engines/mediastation/mediascript/scriptconstants.h
engines/mediastation/mediastation.cpp
engines/mediastation/mediastation.h
engines/mediastation/module.mk
diff --git a/engines/mediastation/actors/printer.cpp b/engines/mediastation/actors/printer.cpp
new file mode 100644
index 00000000000..8973add3165
--- /dev/null
+++ b/engines/mediastation/actors/printer.cpp
@@ -0,0 +1,150 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mediastation/actors/printer.h"
+#include "mediastation/graphics.h"
+#include "mediastation/mediastation.h"
+
+namespace MediaStation {
+
+ScriptValue PrinterActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
+ ScriptValue returnValue;
+ switch (methodId) {
+ case kIsPrinterReadyMethod:
+ ARGCOUNTCHECK(0);
+ returnValue.setToBool(g_engine->getPrintManager()->printerIsReady());
+ break;
+
+ case kPrintActorsMethod:
+ ARGCOUNTCHECK(1);
+ if (args[0].getType() == kScriptValueTypeActorId) {
+ const uint actorId = args[0].asActorId();
+ if (printActor(actorId, true)) {
+ g_engine->getPrintManager()->flushToPrinter();
+ }
+ } else if (args[0].getType() == kScriptValueTypeCollection) {
+ const Collection *actorCollection = args[0].asCollection();
+ if (actorCollection != nullptr) {
+ printActorCollection(*actorCollection);
+ }
+ }
+ break;
+
+ case kPrintScreenMethod:
+ ARGCOUNTCHECK(0);
+ g_engine->getPrintManager()->printScreen();
+ break;
+
+ case kSetPortraitPrintMethod:
+ ARGCOUNTCHECK(1);
+ if (args[0].asString().equalsIgnoreCase("PORTRAIT")) {
+ g_engine->getPrintManager()->_isPortrait = true;
+ } else if (args[0].asString().equalsIgnoreCase("LANDSCAPE")) {
+ g_engine->getPrintManager()->_isPortrait = false;
+ }
+ break;
+
+ case kGetPortraitPrintMethod:
+ ARGCOUNTCHECK(0);
+ returnValue.setToString(g_engine->getPrintManager()->_isPortrait ? "PORTRAIT" : "LANDSCAPE");
+ break;
+
+ case kSetLeftPrintMarginMethod:
+ ARGCOUNTCHECK(1);
+ g_engine->getPrintManager()->_leftMargin = args[0].asFloat();
+ break;
+
+ case kSetTopPrintMarginMethod:
+ ARGCOUNTCHECK(1);
+ g_engine->getPrintManager()->_topMargin = args[0].asFloat();
+ break;
+
+ case kSetRightPrintMarginMethod:
+ ARGCOUNTCHECK(1);
+ g_engine->getPrintManager()->_rightMargin = args[0].asFloat();
+ break;
+
+ case kSetBottomPrintMarginMethod:
+ ARGCOUNTCHECK(1);
+ g_engine->getPrintManager()->_bottomMargin = args[0].asFloat();
+ break;
+
+ case kGetLeftPrintMarginMethod:
+ ARGCOUNTCHECK(0);
+ returnValue.setToFloat(g_engine->getPrintManager()->_leftMargin);
+ break;
+
+ case kGetTopPrintMarginMethod:
+ ARGCOUNTCHECK(0);
+ returnValue.setToFloat(g_engine->getPrintManager()->_topMargin);
+ break;
+
+ case kGetRightPrintMarginMethod:
+ ARGCOUNTCHECK(0);
+ returnValue.setToFloat(g_engine->getPrintManager()->_rightMargin);
+ break;
+
+ case kGetBottomPrintMarginMethod:
+ ARGCOUNTCHECK(0);
+ returnValue.setToFloat(g_engine->getPrintManager()->_bottomMargin);
+ break;
+
+ default:
+ returnValue = SpatialEntity::callMethod(methodId, args);
+ break;
+ }
+
+ return returnValue;
+}
+
+bool PrinterActor::printActor(uint actorId, bool unk1) {
+ if (!unk1) {
+ return false;
+ }
+
+ SpatialEntity *entityToPrint = g_engine->getImtGod()->getSpatialEntityById(actorId);
+ if (entityToPrint == nullptr) {
+ warning("[%s] %s: Actor %u is not spatial and cannot be printed", debugName(), __func__, actorId);
+ return false;
+ }
+
+ PrintManager *printManager = g_engine->getPrintManager();
+ if (printManager == nullptr) {
+ warning("[%s] %s: Print manager has not been initialized", debugName(), __func__);
+ return false;
+ }
+
+ printManager->printSpatialObject(entityToPrint);
+ return true;
+}
+
+void PrinterActor::printActorCollection(const Collection &actors) {
+ for (const ScriptValue &item : actors) {
+ if (item.getType() != kScriptValueTypeActorId) {
+ continue;
+ }
+
+ const uint actorId = item.asActorId();
+ printActor(actorId, true);
+ }
+}
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/actors/printer.h b/engines/mediastation/actors/printer.h
new file mode 100644
index 00000000000..2a751d238c4
--- /dev/null
+++ b/engines/mediastation/actors/printer.h
@@ -0,0 +1,42 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MEDIASTATION_ACTORS_PRINTER_H
+#define MEDIASTATION_ACTORS_PRINTER_H
+
+#include "mediastation/actor.h"
+
+namespace MediaStation {
+
+class PrinterActor : public SpatialEntity {
+public:
+ PrinterActor() : SpatialEntity(kActorTypePrinter) {};
+ virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+
+private:
+ bool printActor(uint actorId, bool unk1);
+ void printActorCollection(const Collection &actors);
+
+};
+
+} // End of namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index a9b584617df..5fbd3532440 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -33,6 +33,7 @@
#include "mediastation/actors/palette.h"
#include "mediastation/actors/image.h"
#include "mediastation/actors/path.h"
+#include "mediastation/actors/printer.h"
#include "mediastation/actors/sound.h"
#include "mediastation/actors/movie.h"
#include "mediastation/actors/sprite.h"
@@ -166,6 +167,10 @@ void ImtGod::readCreateActorData(Chunk &chunk) {
actor = new DiskImageActor();
break;
+ case kActorTypePrinter:
+ actor = new PrinterActor();
+ break;
+
case kActorTypeDotGame:
actor = new DotGameActor();
break;
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index ee127147341..23564470b41 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -1503,4 +1503,25 @@ void VideoDisplayManager::getGammaValues(double &red, double &green, double &blu
blue = _blueGamma;
}
+bool PrintManager::printerIsReady() {
+ // For this stub, we will just always report that â he printer is ready.
+ return true;
+}
+
+void PrintManager::printScreen() {
+ warning("STUB: %s", __func__);
+}
+
+void PrintManager::printSpatialObject(SpatialEntity *entity) {
+ warning("STUB: %s", __func__);
+}
+
+void PrintManager::setSourceSize(Common::Point size) {
+ warning("STUB: %s", __func__);
+}
+
+void PrintManager::flushToPrinter() {
+ warning("STUB: %s", __func__);
+}
+
} // End of namespace MediaStation
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index 844600c3411..609a107dfad 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -146,6 +146,23 @@ private:
Common::Stack<Clip> _clips;
};
+
+class PrintManager : DisplayContext {
+public:
+ bool printerIsReady();
+ void printScreen();
+ void printSpatialObject(SpatialEntity *entity);
+ void setSourceSize(Common::Point size);
+ void flushToPrinter();
+
+ bool _isPortrait = false;
+
+ double _leftMargin = -1;
+ double _rightMargin = -1;
+ double _topMargin = -1;
+ double _bottomMargin = -1;
+};
+
class DisplayUpdateManager {
public:
virtual ~DisplayUpdateManager() {}
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index abec7d8f86e..a0a89b54d6d 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -269,6 +269,32 @@ const char *builtInMethodToStr(BuiltInMethod method) {
return "StartCaching";
case kIsCachingMethod:
return "IsCaching";
+ case kIsPrinterReadyMethod:
+ return "IsPrinterReady";
+ case kPrintActorsMethod:
+ return "PrintActors";
+ case kPrintScreenMethod:
+ return "PrintScreen";
+ case kSetPortraitPrintMethod:
+ return "SetPortraitPrint";
+ case kGetPortraitPrintMethod:
+ return "GetPortraitPrint";
+ case kSetLeftPrintMarginMethod:
+ return "SetLeftPrintMargin";
+ case kSetTopPrintMarginMethod:
+ return "SetTopPrintMargin";
+ case kSetRightPrintMarginMethod:
+ return "SetRightPrintMargin";
+ case kSetBottomPrintMarginMethod:
+ return "SetBottomPrintMargin";
+ case kGetLeftPrintMarginMethod:
+ return "GetLeftPrintMargin";
+ case kGetTopPrintMarginMethod:
+ return "GetTopPrintMargin";
+ case kGetRightPrintMarginMethod:
+ return "GetRightPrintMargin";
+ case kGetBottomPrintMarginMethod:
+ return "GetBottomPrintMargin";
case kIsPausedMethod:
return "SetMultipleSounds/IsPaused";
case kSetMousePositionMethod:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 7fcc8d35de7..07f433dbe2a 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -286,6 +286,19 @@ enum BuiltInMethod {
// between two camera methods and two printer methods.
kOpenLensMethod = 0x15A,
kCloseLensMethod = 0x15B,
+ kIsPrinterReadyMethod = 0x110,
+ kPrintActorsMethod = 0x111,
+ kPrintScreenMethod = 0x112,
+ kSetPortraitPrintMethod = 0x115,
+ kGetPortraitPrintMethod = 0x116,
+ kSetLeftPrintMarginMethod = 0x117,
+ kSetTopPrintMarginMethod = 0x118,
+ kSetRightPrintMarginMethod = 0x119,
+ kSetBottomPrintMarginMethod = 0x11a,
+ kGetLeftPrintMarginMethod = 0x11b,
+ kGetTopPrintMarginMethod = 0x11c,
+ kGetRightPrintMarginMethod = 0x11d,
+ kGetBottomPrintMarginMethod = 0x11e,
// CURSOR METHODS.
kCursorSetMethod = 0xC8,
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 7e510b8fb46..8d756103dd7 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -65,7 +65,7 @@ MediaStationEngine::~MediaStationEngine() {
delete _displayManager;
delete _displayUpdateManager;
delete _functionManager;
- // delete _printManager;
+ delete _printManager;
delete _imtGod;
// delete _streamProfiler;
delete _streamFeedManager;
@@ -163,7 +163,7 @@ Common::Error MediaStationEngine::run() {
_functionManager = new FunctionManager;
_displayUpdateManager = new DisplayUpdateManager;
_displayManager = new VideoDisplayManager(this);
- // _printManager = new PrintManager;
+ _printManager = new PrintManager;
_document = new Document;
DocumentActor *documentActor = new DocumentActor;
_imtGod->addConstructedActor(documentActor);
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index 03035f50e0f..cb75da019af 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -92,6 +92,7 @@ public:
VideoDisplayManager *getDisplayManager() { return _displayManager; }
DisplayUpdateManager *getDisplayUpdateManager() { return _displayUpdateManager; }
+ PrintManager *getPrintManager() { return _printManager; }
CursorManager *getCursorManager() { return _cursorManager; }
FunctionManager *getFunctionManager() { return _functionManager; }
RootStage *getRootStage() { return _stageDirector->getRootStage(); }
@@ -148,7 +149,7 @@ private:
FunctionManager *_functionManager = nullptr;
DisplayUpdateManager *_displayUpdateManager = nullptr;
VideoDisplayManager *_displayManager = nullptr;
- // PrintManager *_printManager = nullptr;
+ PrintManager *_printManager = nullptr;
Document *_document = nullptr;
CursorManager *_cursorManager = nullptr;
StageDirector *_stageDirector = nullptr;
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 102946cb517..3352d0e9baf 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -13,6 +13,7 @@ MODULE_OBJS = \
actors/movie.o \
actors/palette.o \
actors/path.o \
+ actors/printer.o \
actors/screen.o \
actors/sound.o \
actors/sprite.o \
Commit: 0fcbd2cc2c3591fe3cd19d3b6df93b9d7d765ae1
https://github.com/scummvm/scummvm/commit/0fcbd2cc2c3591fe3cd19d3b6df93b9d7d765ae1
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:55:59-04:00
Commit Message:
MEDIASTATION: Get text input working for Puzzle Castle
Assisted-by: gpt-5.5 claude-sonnet-4.5
Changed paths:
engines/mediastation/actor.h
engines/mediastation/actors/stage.cpp
engines/mediastation/actors/stage.h
engines/mediastation/actors/text.cpp
engines/mediastation/actors/text.h
engines/mediastation/debugchannels.h
engines/mediastation/detection.cpp
engines/mediastation/events.cpp
engines/mediastation/events.h
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index e1c54463a1e..fe036b5f0fc 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -292,7 +292,7 @@ public:
MouseActorState &state,
bool clipMouseEvents) { return kNoFlag; }
virtual uint16 findActorToAcceptKeyboardEvents(
- uint16 asciiCode,
+ uint16 charCode,
uint16 eventMask,
MouseActorState &state) { return kNoFlag; }
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index 274a28b76fa..707f7cb1415 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -613,13 +613,13 @@ uint16 StageActor::findActorToAcceptMouseEvents(
}
uint16 StageActor::findActorToAcceptKeyboardEvents(
- uint16 asciiCode,
+ uint16 charCode,
uint16 eventMask,
MouseActorState &state) {
uint16 result = 0;
for (SpatialEntity *child : _children) {
- uint16 handledEvents = child->findActorToAcceptKeyboardEvents(asciiCode, eventMask, state);
+ uint16 handledEvents = child->findActorToAcceptKeyboardEvents(charCode, eventMask, state);
if (handledEvents != 0) {
eventMask &= ~handledEvents;
result |= handledEvents;
@@ -840,7 +840,7 @@ void StageDirector::handleMouseEvent(const MouseEvent &event) {
void StageDirector::handleKeyboardEvent(const KeyboardEvent &event) {
MouseActorState state;
- uint16 flags = _rootStage->findActorToAcceptKeyboardEvents(event.keyCode, kKeyDownFlag, state);
+ uint16 flags = _rootStage->findActorToAcceptKeyboardEvents(event.getMediaStationCharCode(), kKeyDownFlag, state);
if (flags & kKeyDownFlag) {
debugC(5, kDebugEvents, "%s: Dispatching to %s from root stage", __func__, state.keyDown->debugName());
state.keyDown->keyboardEvent(event);
diff --git a/engines/mediastation/actors/stage.h b/engines/mediastation/actors/stage.h
index 33b3a7ffd5f..0bca9b06cb7 100644
--- a/engines/mediastation/actors/stage.h
+++ b/engines/mediastation/actors/stage.h
@@ -91,7 +91,7 @@ public:
MouseActorState &state,
bool clipMouseEvents) override;
virtual uint16 findActorToAcceptKeyboardEvents(
- uint16 asciiCode,
+ uint16 charCode,
uint16 eventMask,
MouseActorState &state) override;
virtual void currentMousePosition(Common::Point &point) override;
diff --git a/engines/mediastation/actors/text.cpp b/engines/mediastation/actors/text.cpp
index 4a91b25257a..e93feb70d79 100644
--- a/engines/mediastation/actors/text.cpp
+++ b/engines/mediastation/actors/text.cpp
@@ -19,6 +19,7 @@
*
*/
+#include "mediastation/debugchannels.h"
#include "mediastation/actors/text.h"
#include "mediastation/mediastation.h"
@@ -60,8 +61,8 @@ void TextActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
case kActorHeaderTextAcceptedCharRangeWithOffset: {
uint firstCharCode = chunk.readTypedUint16();
uint lastCharCode = chunk.readTypedUint16();
- uint charCodeOffset = chunk.readTypedUint16();
- addAcceptedChars(firstCharCode, lastCharCode, charCodeOffset);
+ uint standardizedCharCode = chunk.readTypedUint16();
+ addAcceptedChars(firstCharCode, lastCharCode, standardizedCharCode);
break;
}
@@ -77,7 +78,7 @@ void TextActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
break;
case kActorHeaderTextCursorIsVisible:
- _cursorIsVisible = static_cast<bool>(chunk.readTypedByte());
+ _cursorIsVisible = static_cast<bool>(chunk.readTypedUint16());
break;
case kActorHeaderTextConstrainToWidth:
@@ -168,7 +169,7 @@ ScriptValue TextActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptVa
case kGetLastPressedCharCodeMethod:
ARGCOUNTCHECK(0);
- returnValue.setToFloat(_pressedCharCode);
+ returnValue.setToFloat(_lastCharCode);
break;
case kTextGetCursorPositionMethod:
@@ -243,10 +244,10 @@ ScriptValue TextActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptVa
case kTextGetTranslatedCharCode: {
ARGCOUNTCHECK(1);
- uint charId = args[0].asParamToken();
- uint translatedChar = _acceptedChars.getValOrDefault(charId, 0);
- if (translatedChar != 0) {
- returnValue.setToFloat(translatedChar);
+ uint charCode = args[0].asParamToken();
+ uint standardizedCharCode = _standardizedChars.getValOrDefault(charCode, 0);
+ if (standardizedCharCode != 0) {
+ returnValue.setToFloat(standardizedCharCode);
} else {
// Character not found, so return the input as-is.
returnValue = args[0];
@@ -256,35 +257,35 @@ ScriptValue TextActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptVa
case kTextAddAcceptedCharsMethod: {
ARGCOUNTMIN(2);
- uint startCharId = static_cast<uint>(args[0].asFloat());
- uint endCharId = static_cast<uint>(args[1].asFloat());
- uint category = 0;
+ uint startCharCode = static_cast<uint>(args[0].asFloat());
+ uint endCharCode = static_cast<uint>(args[1].asFloat());
+ uint firstStandardizedCharCode = 0;
if (args.size() >= 3) {
- category = static_cast<uint>(args[2].asFloat());
+ firstStandardizedCharCode = static_cast<uint>(args[2].asFloat());
}
- addAcceptedChars(startCharId, endCharId, category);
+ addAcceptedChars(startCharCode, endCharCode, firstStandardizedCharCode);
break;
}
case kTextIsCharacterAcceptedMethod: {
ARGCOUNTCHECK(1);
- uint charId = static_cast<uint>(args[0].asFloat());
- bool isAccepted = _acceptedChars.contains(charId);
+ uint charCode = static_cast<uint>(args[0].asFloat());
+ bool isAccepted = _standardizedChars.contains(charCode);
returnValue.setToBool(isAccepted);
break;
}
case kTextEnableDisableCharacterMethod: {
ARGCOUNTCHECK(2);
- uint charId = static_cast<uint>(args[0].asFloat());
+ uint charCode = static_cast<uint>(args[0].asFloat());
bool shouldEnable = static_cast<bool>(args[1].asBool());
if (shouldEnable) {
// Mark character as accepted.
- addAcceptedChars(charId, charId, 0);
+ addAcceptedChars(charCode, charCode, 0);
} else {
// No longer mark character as accepted.
- _acceptedChars.erase(charId);
+ _standardizedChars.erase(charCode);
}
break;
}
@@ -345,22 +346,109 @@ uint16 TextActor::findActorToAcceptKeyboardEvents(uint16 charCode, uint16 eventM
uint16 result = 0;
if (_loadIsComplete && (eventMask & kKeyDownFlag) && _isEditable) {
// If we have accepted character restrictions, check if character is valid.
- if (!_acceptedChars.empty()) {
- if (!_acceptedChars.contains(charCode)) {
+ if (!_standardizedChars.empty()) {
+ if (_standardizedChars.contains(charCode)) {
+ debugC(5, kDebugText, "[%s] %s: Accepted char %d", debugName(), __func__, charCode);
+ } else {
+ debugC(5, kDebugText, "[%s] %s: REJECTED char %d", debugName(), __func__, charCode);
return 0;
}
+ } else {
+ debugC(5, kDebugText, "[%s] %s: Auto-accepted char %d", debugName(), __func__, charCode);
}
state.keyDown = this;
- result = kNoFlag;
+ result = kKeyDownFlag;
}
return result;
}
void TextActor::keyboardEvent(const KeyboardEvent &event) {
- // TODO: Implement this once we have a title that actually uses it.
- warning("STUB: %s", __func__);
+ uint_least16_t charCode = event.getMediaStationCharCode();
+ uint standardizedCharCode = _standardizedChars.getValOrDefault(charCode, 0);
+ if (standardizedCharCode == 0) {
+ standardizedCharCode = charCode;
+ }
+ debugC(5, kDebugText, "[%s] %s: charCode: %d (standardized: %d)", debugName(), __func__, _lastCharCode, standardizedCharCode);
+ _lastCharCode = standardizedCharCode;
+
+ ScriptValue charCodeValue;
+ charCodeValue.setToFloat(_lastCharCode);
+ ScriptValue defaultCharCodeValue;
+ defaultCharCodeValue.setToFloat(0.0);
+
+ Common::String updatedText = _text;
+ bool isDirty = false;
+
+ if (standardizedCharCode == kBackspaceCharCode) {
+ if (_cursorPosition != 0) {
+ updatedText.deleteChar(_cursorPosition - 1);
+ _cursorPosition--;
+ isDirty = true;
+ _text = updatedText;
+ debugC(5, kDebugText, "[%s] %s: text: %s", debugName(), __func__, updatedText.c_str());
+
+ if (hasScriptResponse(kTextInputEvent, charCodeValue)) {
+ runScriptResponseIfExists(kTextInputEvent, charCodeValue);
+ } else {
+ runScriptResponseIfExists(kTextInputEvent, defaultCharCodeValue);
+ }
+ }
+
+ } else if (standardizedCharCode == kLeftArrowCharCode) {
+ if (_cursorPosition != 0) {
+ _cursorPosition--;
+ if (_cursorIsVisible) {
+ isDirty = true;
+ }
+ }
+
+ } else if (standardizedCharCode == kRightArrowCharCode) {
+ if (_cursorPosition < _text.size()) {
+ _cursorPosition++;
+ if (_cursorIsVisible) {
+ isDirty = true;
+ }
+ }
+
+ } else {
+ // This is likely a normal ASCII character, so try to insert it in the string.
+ if (_text.size() < _maxLength) {
+ if (_overwriteMode && (_cursorPosition < _text.size())) {
+ updatedText.deleteChar(_cursorPosition);
+ }
+
+ updatedText.insertChar(static_cast<char>(standardizedCharCode), _cursorPosition);
+ debugC(5, kDebugText, "[%s] %s: text: %s", debugName(), __func__, updatedText.c_str());
+ if (_constrainToWidth && (_fontActor != nullptr)) {
+ const int16 maxWidth = getBbox().width();
+ const int16 pixelLength = calcPixelLength(updatedText);
+ if (pixelLength > maxWidth) {
+ warning("%s: Input text exceeds max length", __func__);
+ runScriptResponseIfExists(kTextErrorEvent);
+ }
+ }
+
+ _cursorPosition++;
+ isDirty = true;
+ _text = updatedText;
+
+ if (hasScriptResponse(kTextInputEvent, charCodeValue)) {
+ runScriptResponseIfExists(kTextInputEvent, charCodeValue);
+ } else {
+ runScriptResponseIfExists(kTextInputEvent, defaultCharCodeValue);
+ }
+
+ } else {
+ warning("%s: Input text exceeds max length", __func__);
+ runScriptResponseIfExists(kTextErrorEvent);
+ }
+ }
+
+ if (isDirty) {
+ invalidateLocalBounds();
+ }
}
bool TextActor::hasScriptResponse(EventType eventType, const ScriptValue &arg) const {
@@ -391,18 +479,18 @@ void TextActor::setText() {
// Apply character translation if we have any.
for (uint positionInString = 0; positionInString < _text.size(); positionInString++) {
char currentChar = _text[positionInString];
- uint translatedChar = _acceptedChars.getValOrDefault(currentChar);
- if (translatedChar != 0) {
- _text.setChar(translatedChar, positionInString);
- }
+ uint standardizedCharCode = _standardizedChars.getValOrDefault(currentChar, currentChar);
+ _text.setChar(standardizedCharCode, positionInString);
}
}
-void TextActor::addAcceptedChars(uint firstCharCode, uint lastCharCode, uint charCodeOffset) {
+void TextActor::addAcceptedChars(uint firstCharCode, uint lastCharCode, uint standardizedCharCode) {
for (uint charCode = firstCharCode; charCode <= lastCharCode; charCode++) {
- _acceptedChars.setVal(charCode, charCodeOffset);
- if (charCodeOffset != 0) {
- charCodeOffset++;
+ if (!_standardizedChars.contains(charCode)) {
+ _standardizedChars.setVal(charCode, standardizedCharCode);
+ }
+ if (standardizedCharCode != 0) {
+ standardizedCharCode++;
}
}
}
@@ -448,7 +536,7 @@ void TextActor::drawCharacter(FontCharacter *glyph, int16 x, int16 y, DisplayCon
}
void TextActor::drawCursor(int16 x, int16 y, DisplayContext &displayContext) {
- FontCharacter *cursorChar = _fontActor->lookupCharacter(CURSOR_CHAR_ID);
+ FontCharacter *cursorChar = _fontActor->lookupCharacter(kCursorCharCode);
if (cursorChar != nullptr) {
drawCharacter(cursorChar, x, y, displayContext);
}
@@ -472,7 +560,7 @@ int16 TextActor::calcPixelLength(const Common::String &text) {
// Add cursor width if we are needing to show the cursor.
if (_cursorPosition == _text.size() && _cursorIsVisible) {
- FontCharacter *cursorChar = _fontActor->lookupCharacter(CURSOR_CHAR_ID);
+ FontCharacter *cursorChar = _fontActor->lookupCharacter(kCursorCharCode);
if (cursorChar != nullptr) {
totalXOffset += cursorChar->width();
}
diff --git a/engines/mediastation/actors/text.h b/engines/mediastation/actors/text.h
index f0654a18909..10123bd32ef 100644
--- a/engines/mediastation/actors/text.h
+++ b/engines/mediastation/actors/text.h
@@ -56,19 +56,19 @@ public:
virtual void keyboardEvent(const KeyboardEvent &event) override;
private:
- static const uint CURSOR_CHAR_ID = 0x104;
bool _isEditable = false;
Common::String _text;
uint _maxLength = 0;
FontActor *_fontActor = nullptr;
TextJustification _justification = kTextJustificationLeft;
TextPosition _position = kTextPositionTop;
- Common::HashMap<uint, uint> _acceptedChars;
uint _cursorPosition = 0;
- uint _pressedCharCode = 0;
+ uint _lastCharCode = 0;
bool _cursorIsVisible = false;
bool _constrainToWidth = false;
bool _overwriteMode = false;
+ // This lets us transparently convert lowercase to uppercase, for instance.
+ Common::HashMap<uint, uint> _standardizedChars;
void setText();
void addAcceptedChars(uint firstCharCode, uint lastCharCode, uint charCodeOffset = 0);
diff --git a/engines/mediastation/debugchannels.h b/engines/mediastation/debugchannels.h
index bf75c59acda..e8f0ee8a66e 100644
--- a/engines/mediastation/debugchannels.h
+++ b/engines/mediastation/debugchannels.h
@@ -35,6 +35,7 @@ enum DebugChannels {
kDebugCamera,
kDebugPath,
kDebugScan,
+ kDebugText,
kDebugScript,
kDebugEvents,
kDebugLoading,
diff --git a/engines/mediastation/detection.cpp b/engines/mediastation/detection.cpp
index ccfd49d637c..177dec22fa7 100644
--- a/engines/mediastation/detection.cpp
+++ b/engines/mediastation/detection.cpp
@@ -37,6 +37,7 @@ const DebugChannelDef MediaStationMetaEngineDetection::debugFlagList[] = {
{ MediaStation::kDebugCamera, "camera", "Camera panning debug level" },
{ MediaStation::kDebugPath, "path", "Pathfinding debug level" },
{ MediaStation::kDebugScan, "scan", "Scan for unrecognised games" },
+ { MediaStation::kDebugText, "text", "Text display" },
{ MediaStation::kDebugScript, "script", "Enable debug script dump" },
{ MediaStation::kDebugEvents, "events", "Events processing" },
{ MediaStation::kDebugLoading, "loading", "File loading" },
diff --git a/engines/mediastation/events.cpp b/engines/mediastation/events.cpp
index d5305ebd5ee..b3d88b711e5 100644
--- a/engines/mediastation/events.cpp
+++ b/engines/mediastation/events.cpp
@@ -90,8 +90,40 @@ Common::String MouseEvent::debugString() const {
}
Common::String KeyboardEvent::debugString() const {
- return Common::String::format("%s %s (keyCode: %u)",
- eventClassToStr(eventClass), eventTypeToStr(type), keyCode);
+ return Common::String::format("%s %s (state: { keycode: %u, ascii: %u, flags: 0x%02x })",
+ eventClassToStr(eventClass), eventTypeToStr(type),
+ static_cast<uint>(state.keycode), state.ascii, state.flags);
+}
+
+uint16 KeyboardEvent::getMediaStationCharCode() const {
+ uint16 result = state.ascii;
+ switch (state.keycode) {
+ case Common::KEYCODE_BACKSPACE:
+ result = static_cast<uint16>(kBackspaceCharCode);
+ break;
+
+ case Common::KEYCODE_LEFT:
+ result = static_cast<uint16>(kLeftArrowCharCode);
+ break;
+
+ case Common::KEYCODE_RIGHT:
+ result = static_cast<uint16>(kRightArrowCharCode);
+ break;
+
+ case Common::KEYCODE_UP:
+ result = static_cast<uint16>(kUpArrowCharCode);
+ break;
+
+ case Common::KEYCODE_DOWN:
+ result = static_cast<uint16>(kDownArrowCharCode);
+ break;
+
+ default:
+ // This is explicitly a no-op.
+ break;
+ }
+
+ return result;
}
void EventLoop::run() {
@@ -208,7 +240,7 @@ void MediaStationEngine::dispatchOneSystemEvent(const Common::Event &event) {
}
case Common::EVENT_KEYDOWN: {
- KeyboardEvent keyboardEvent(kKeyDownEvent, event.kbd.ascii);
+ KeyboardEvent keyboardEvent(kKeyDownEvent, event.kbd);
_stageDirector->handleKeyboardEvent(keyboardEvent);
break;
}
diff --git a/engines/mediastation/events.h b/engines/mediastation/events.h
index 6db1d4d5fb2..04d2e021111 100644
--- a/engines/mediastation/events.h
+++ b/engines/mediastation/events.h
@@ -131,13 +131,28 @@ struct MouseEvent : public Event {
Common::String debugString() const override;
};
+enum SpecialCharCodes {
+ kBackspaceCharCode = 0x08,
+ kLeftArrowCharCode = 0x100,
+ kRightArrowCharCode = 0x101,
+ kUpArrowCharCode = 0x102,
+ kDownArrowCharCode = 0x103,
+ kCursorCharCode = 0x104,
+};
+
struct KeyboardEvent : public Event {
- uint16 keyCode = 0;
+ Common::KeyState state;
- KeyboardEvent(EventType eventType_, uint16 keyCode_)
- : Event(kEventClassKeyboard, eventType_), keyCode(keyCode_) {}
+ KeyboardEvent(EventType eventType_, Common::KeyState state_)
+ : Event(kEventClassKeyboard, eventType_), state(state_) {}
Event *clone() const override { return new KeyboardEvent(*this); }
Common::String debugString() const override;
+
+ // Scripts and text actors use a 16-bit field for their char codes,
+ // which also encapsulates non-text keys like backspace and arrow keys.
+ // This converts the platform-independent ScummVM keystate into what scripts
+ // want. See MAC_App::doKeyDownEvent in the 68k disassembly.
+ uint16 getMediaStationCharCode() const;
};
enum PreDisplaySyncState {
Commit: e608521bed3cd769734dd16bcba9a88c9eb4e5b3
https://github.com/scummvm/scummvm/commit/e608521bed3cd769734dd16bcba9a88c9eb4e5b3
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:56:00-04:00
Commit Message:
MEDIASTATION: Implement key events on hotspots
Changed paths:
engines/mediastation/actor.cpp
engines/mediastation/actor.h
engines/mediastation/actors/hotspot.cpp
engines/mediastation/actors/hotspot.h
engines/mediastation/actors/text.cpp
engines/mediastation/actors/text.h
diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index d46e7210071..daee8e3f0f2 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -214,6 +214,22 @@ ScriptResponse *Actor::findNextTimeScriptResponseAfter(uint32 after) const {
return nullptr;
}
+bool Actor::hasScriptResponse(EventType eventType, const ScriptValue &arg) const {
+ const Common::Array<ScriptResponse *> &scriptResponses = _scriptResponses.getValOrDefault(eventType);
+ for (const ScriptResponse *scriptResponse : scriptResponses) {
+ const ScriptValue &argToCheck = scriptResponse->_argumentValue;
+
+ if (arg.getType() != argToCheck.getType()) {
+ continue;
+ }
+
+ if (arg == argToCheck) {
+ return true;
+ }
+ }
+ return false;
+}
+
void Actor::runScriptResponseIfExists(EventType eventType, const ScriptValue &arg) {
const Common::Array<ScriptResponse *> &scriptResponses = _scriptResponses.getValOrDefault(eventType);
for (ScriptResponse *scriptResponse : scriptResponses) {
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index fe036b5f0fc..de3dfa6bb71 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -225,6 +225,7 @@ public:
virtual void onEvent(const ActorEvent &event);
ScriptResponse *findNextTimeScriptResponseAfter(uint32 after) const;
+ bool hasScriptResponse(EventType eventType, const ScriptValue &arg) const;
void runScriptResponseIfExists(EventType eventType, const ScriptValue &arg);
void runScriptResponseIfExists(EventType eventType);
diff --git a/engines/mediastation/actors/hotspot.cpp b/engines/mediastation/actors/hotspot.cpp
index 357a009919d..4a856e697a9 100644
--- a/engines/mediastation/actors/hotspot.cpp
+++ b/engines/mediastation/actors/hotspot.cpp
@@ -153,6 +153,26 @@ uint16 HotspotActor::findActorToAcceptMouseEvents(
return result;
}
+uint16 HotspotActor::findActorToAcceptKeyboardEvents(uint16 charCode, uint16 eventMask, MouseActorState &state) {
+ if (!_isActive || !_loadIsComplete) {
+ return 0;
+ }
+
+ if (eventMask & kKeyDownFlag) {
+ ScriptValue charCodeValue;
+ charCodeValue.setToFloat(charCode);
+ ScriptValue wildcardValue;
+ wildcardValue.setToFloat(0.0);
+
+ if (hasScriptResponse(kKeyDownEvent, charCodeValue) || hasScriptResponse(kKeyDownEvent, wildcardValue)) {
+ state.keyDown = this;
+ return kKeyDownFlag;
+ }
+ }
+
+ return 0;
+}
+
void HotspotActor::activate() {
if (!_isActive) {
_isActive = true;
@@ -221,6 +241,20 @@ void HotspotActor::mouseMovedEvent(const MouseEvent &event) {
runScriptResponseIfExists(kMouseMovedEvent);
}
+void HotspotActor::keyboardEvent(const KeyboardEvent &event) {
+ ScriptValue charCodeValue;
+ charCodeValue.setToFloat(event.getMediaStationCharCode());
+
+ if (hasScriptResponse(event.type, charCodeValue)) {
+ runScriptResponseIfExists(event.type, charCodeValue);
+ } else {
+ // Otherwise, try to do the wildcard match.
+ ScriptValue wildcardValue;
+ wildcardValue.setToFloat(0.0);
+ runScriptResponseIfExists(event.type, wildcardValue);
+ }
+}
+
void HotspotActor::mouseExitedEvent(const MouseEvent &event) {
if (!_isActive) {
warning("[%s] %s: Inactive", debugName(), __func__);
diff --git a/engines/mediastation/actors/hotspot.h b/engines/mediastation/actors/hotspot.h
index 0ceac8f326d..730bca91cb3 100644
--- a/engines/mediastation/actors/hotspot.h
+++ b/engines/mediastation/actors/hotspot.h
@@ -46,6 +46,10 @@ public:
uint16 eventMask,
MouseActorState &state,
bool clipMouseEvents) override;
+ virtual uint16 findActorToAcceptKeyboardEvents(
+ uint16 charCode,
+ uint16 eventMask,
+ MouseActorState &state) override;
void activate();
void deactivate();
@@ -55,6 +59,7 @@ public:
virtual void mouseEnteredEvent(const MouseEvent &event) override;
virtual void mouseExitedEvent(const MouseEvent &event) override;
virtual void mouseMovedEvent(const MouseEvent &event) override;
+ virtual void keyboardEvent(const KeyboardEvent &event) override;
uint _cursorResourceId = 0;
diff --git a/engines/mediastation/actors/text.cpp b/engines/mediastation/actors/text.cpp
index e93feb70d79..43b18a1831a 100644
--- a/engines/mediastation/actors/text.cpp
+++ b/engines/mediastation/actors/text.cpp
@@ -451,22 +451,6 @@ void TextActor::keyboardEvent(const KeyboardEvent &event) {
}
}
-bool TextActor::hasScriptResponse(EventType eventType, const ScriptValue &arg) const {
- const Common::Array<ScriptResponse *> &scriptResponses = _scriptResponses.getValOrDefault(eventType);
- for (const ScriptResponse *scriptResponse : scriptResponses) {
- const ScriptValue &argToCheck = scriptResponse->_argumentValue;
-
- if (arg.getType() != argToCheck.getType()) {
- continue;
- }
-
- if (arg == argToCheck) {
- return true;
- }
- }
- return false;
-}
-
void TextActor::setText() {
// Remove double-quotes if they're the first or last characters.
if (_text.firstChar() == '"') {
diff --git a/engines/mediastation/actors/text.h b/engines/mediastation/actors/text.h
index 10123bd32ef..d19aad6fc0b 100644
--- a/engines/mediastation/actors/text.h
+++ b/engines/mediastation/actors/text.h
@@ -72,7 +72,6 @@ private:
void setText();
void addAcceptedChars(uint firstCharCode, uint lastCharCode, uint charCodeOffset = 0);
- bool hasScriptResponse(EventType eventType, const ScriptValue &arg) const;
int16 calcStartingXPosition();
int16 calcBaseline();
Commit: 0591485c99ff1de146c7b0be6a13b969fda8a93f
https://github.com/scummvm/scummvm/commit/0591485c99ff1de146c7b0be6a13b969fda8a93f
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T19:56:00-04:00
Commit Message:
MEDIASTATION: Implement hardcoded stalking minigame for Lion King
Assisted-by: gpt-5.5 claude-sonnet-4.5
Changed paths:
A engines/mediastation/minigames/stalkingzazu.cpp
A engines/mediastation/minigames/stalkingzazu.h
A engines/mediastation/statemachine.h
engines/mediastation/actor.cpp
engines/mediastation/actor.h
engines/mediastation/actors/sound.h
engines/mediastation/actors/sprite.h
engines/mediastation/context.cpp
engines/mediastation/debugchannels.h
engines/mediastation/detection.cpp
engines/mediastation/mediascript/scriptconstants.cpp
engines/mediastation/mediascript/scriptconstants.h
engines/mediastation/minigames/dotgame.cpp
engines/mediastation/module.mk
diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index daee8e3f0f2..f0dcb4b9ec4 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -52,8 +52,8 @@ const char *actorTypeToStr(ActorType type) {
return "Cursor";
case kActorTypeSprite:
return "Sprite";
- case kActorTypeLKZazu:
- return "LKZazu";
+ case kActorTypeStalkingZazu:
+ return "StalkingZazu";
case kActorTypeDotGame:
return "DotGame";
case kActorTypeDocument:
@@ -557,7 +557,7 @@ void SpatialEntity::moveTo(int16 x, int16 y) {
debugC(3, kDebugGraphics, "[%s] %s: (%d, %d) -> (%d, %d)", debugName(), __func__,
_originalBoundingBox.origin().x, _originalBoundingBox.origin().y, x, y);
- if (dest == _boundingBox.origin()) {
+ if (dest == _originalBoundingBox.origin()) {
// We aren't actually moving anywhere.
return;
}
@@ -583,7 +583,7 @@ void SpatialEntity::moveToCentered(int16 x, int16 y) {
}
void SpatialEntity::setBounds(const Common::Rect &bounds) {
- if (_boundingBox == bounds) {
+ if (_originalBoundingBox == bounds) {
// We aren't actually moving anywhere.
return;
}
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index de3dfa6bb71..2e276bb96d5 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -47,7 +47,7 @@ enum ActorType {
kActorTypeImage = 0x0007, // IMG
kActorTypeHotspot = 0x000b, // HSP
kActorTypeSprite = 0x000e, // SPR
- kActorTypeLKZazu = 0x000f,
+ kActorTypeStalkingZazu = 0x000f,
kActorTypeDotGame = 0x0010,
kActorTypeDocument = 0x0011,
kActorTypeDiskImage = 0x001d,
@@ -154,6 +154,19 @@ enum ActorHeaderSectionType {
kActorHeaderDotGameSpeed = 0x0517,
kActorHeaderDotGameLineThickness = 0x0518,
kActorHeaderDotGameColor = 0x0519,
+
+ // STALKING ZAZU FIELDS.
+ kActorHeaderStalkingZazuSpriteId = 0x03ec,
+ kActorHeaderStalkingZazuDirections = 0x03ed,
+ kActorHeaderStalkingZazuField = 0x03ee,
+ kActorHeaderStalkingZazuObstacleActorId = 0x03ef,
+ kActorHeaderStalkingZazuUnkX = 0x03f0,
+ kActorHeaderStalkingZazuUnkY = 0x03f1,
+ kActorHeaderStalkingZazuUnkSound1 = 0x03f2,
+ kActorHeaderStalkingZazuUnkSound2 = 0x03f3,
+ kActorHeaderStalkingZazuAudioEnabled = 0x03f4,
+ kActorHeaderStalkingZazuObstaclesToShow = 0x03f5,
+ kActorHeaderStalkingZazuUnkPoint = 0x03f6,
};
enum CylindricalWrapMode : int;
@@ -282,6 +295,8 @@ public:
virtual Common::Rect getBbox() const { return _boundingBox; }
int zIndex() const { return _zIndex; }
void moveTo(int16 x, int16 y);
+ void moveToCentered(int16 x, int16 y);
+ void setZIndex(int zIndex);
virtual void currentMousePosition(Common::Point &point);
virtual void invalidateMouse();
@@ -325,9 +340,7 @@ protected:
bool _hasTransparency = false;
StageActor *_parentStage = nullptr;
- void moveToCentered(int16 x, int16 y);
void setBounds(const Common::Rect &bounds);
- void setZIndex(int zIndex);
virtual void setMousePosition(int16 x, int16 y);
virtual void setDissolveFactor(double dissolveFactor);
diff --git a/engines/mediastation/actors/sound.h b/engines/mediastation/actors/sound.h
index a53b7b84f1c..7d1a5d604b8 100644
--- a/engines/mediastation/actors/sound.h
+++ b/engines/mediastation/actors/sound.h
@@ -44,6 +44,11 @@ public:
virtual void timerEvent(const TimerEvent &event) override;
virtual void soundPlayStateChanged(SoundPlayState state, SoundStopReason why) override;
+ void start();
+ void stop();
+ void pause();
+ void resume(bool restart);
+
private:
ImtStreamFeed *_streamFeed = nullptr;
bool _isLoadedFromChunk = false;
@@ -51,11 +56,6 @@ private:
SoundPlayState _playState = kSoundPlayStateStopped;
AudioSequence _sequence;
- void start();
- void stop();
- void pause();
- void resume(bool restart);
-
void openStream();
};
diff --git a/engines/mediastation/actors/sprite.h b/engines/mediastation/actors/sprite.h
index d4f6cfccec8..406ca085bec 100644
--- a/engines/mediastation/actors/sprite.h
+++ b/engines/mediastation/actors/sprite.h
@@ -79,6 +79,10 @@ public:
virtual void onEvent(const ActorEvent &event) override;
virtual void timerEvent(const TimerEvent &event) override;
+ bool activateNextFrame();
+ bool activatePreviousFrame();
+ void setCurrentClip(uint clipId);
+
private:
const uint DEFAULT_FORWARD_CLIP_ID = 0x4B0;
const uint DEFAULT_BACKWARD_CLIP_ID = 0x4B1;
@@ -96,10 +100,6 @@ private:
void play();
void stop();
- void setCurrentClip(uint clipId);
-
- bool activateNextFrame();
- bool activatePreviousFrame();
void dirtyIfVisible();
void setCurrentFrameToInitial();
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 5fbd3532440..075f932f3d9 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -44,6 +44,7 @@
#include "mediastation/actors/font.h"
#include "mediastation/actors/text.h"
#include "mediastation/minigames/dotgame.h"
+#include "mediastation/minigames/stalkingzazu.h"
namespace MediaStation {
@@ -135,6 +136,10 @@ void ImtGod::readCreateActorData(Chunk &chunk) {
actor = new SpriteMovieActor();
break;
+ case kActorTypeStalkingZazu:
+ actor = new StalkingZazuActor();
+ break;
+
case kActorTypeCanvas:
actor = new CanvasActor();
break;
diff --git a/engines/mediastation/debugchannels.h b/engines/mediastation/debugchannels.h
index e8f0ee8a66e..25ef803ea15 100644
--- a/engines/mediastation/debugchannels.h
+++ b/engines/mediastation/debugchannels.h
@@ -40,6 +40,7 @@ enum DebugChannels {
kDebugEvents,
kDebugLoading,
kDebugSpriteMovie,
+ kDebugMinigame,
};
} // End of namespace MediaStation
diff --git a/engines/mediastation/detection.cpp b/engines/mediastation/detection.cpp
index 177dec22fa7..0c3cc504dca 100644
--- a/engines/mediastation/detection.cpp
+++ b/engines/mediastation/detection.cpp
@@ -42,6 +42,7 @@ const DebugChannelDef MediaStationMetaEngineDetection::debugFlagList[] = {
{ MediaStation::kDebugEvents, "events", "Events processing" },
{ MediaStation::kDebugLoading, "loading", "File loading" },
{ MediaStation::kDebugSpriteMovie, "spritemovie", "Sprite movie debug level" },
+ { MediaStation::kDebugMinigame, "minigame", "Hardcoded minigames" },
DEBUG_CHANNEL_END
};
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index a0a89b54d6d..de86291f59d 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -476,14 +476,24 @@ const char *builtInMethodToStr(BuiltInMethod method) {
return "StopLoad";
case kIsRectInMemoryMethod:
return "IsRectInMemory";
- case kDotGameResetMethod:
- return "DotGameReset";
- case kDotGameShowMethod:
- return "DotGameShow";
- case kDotGameHideMethod:
- return "DotGameHide";
+ case kMinigameResetMethod:
+ return "MinigameReset";
+ case kMinigameActivateMethod:
+ return "MinigameActivate";
+ case kMinigameDeactivateMethod:
+ return "MinigameDeactivate";
case kDotGameHitMethod:
return "DotGameHit";
+ case kStalkingStartZazuLookingMethod:
+ return "StalkingSetLookOn";
+ case kStalkingStopZazuLookingMethod:
+ return "StalkingSetLookOff";
+ case kStalkingGetZazuLookDirectionMethod:
+ return "StalkingGetLookDirection";
+ case kStalkingEnableAudio:
+ return "StalkingEnableAudio";
+ case kStalkingDisableAudio:
+ return "StalkingDisableAudio";
default:
return "UNKNOWN";
}
@@ -537,8 +547,12 @@ const char *eventTypeToStr(EventType type) {
return "MovieFailure";
case kSpriteMovieEndEvent:
return "SpriteMovieEnd";
- case kDotGameCompleteEvent:
- return "DotGameComplete";
+ case kMinigameSuccessEvent:
+ return "MinigameSuccess";
+ case kMinigameDefeatEvent:
+ return "MinigameDefeat";
+ case kMinigameCrouchedEvent:
+ return "MinigameCrouched";
case kScreenExitEvent:
return "ScreenExit";
case kPathStepEvent:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 07f433dbe2a..8e1e9c3e7e4 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -309,11 +309,18 @@ enum BuiltInMethod {
kStopLoadMethod = 0x168,
kIsRectInMemoryMethod = 0x16A,
- // DOT GAME ACTOR METHODS.
- kDotGameResetMethod = 0xE2,
- kDotGameShowMethod = 0xE3,
- kDotGameHideMethod = 0xE4,
+ // MINIGAME ACTOR METHODS.
+ kMinigameResetMethod = 0xE2,
+ kMinigameActivateMethod = 0xE3,
+ kMinigameDeactivateMethod = 0xE4,
kDotGameHitMethod = 0xE5,
+
+ // STALKING ACTOR METHODS.
+ kStalkingStartZazuLookingMethod = 0xDF,
+ kStalkingStopZazuLookingMethod = 0xE0,
+ kStalkingGetZazuLookDirectionMethod = 0xE1,
+ kStalkingEnableAudio = 0xE7,
+ kStalkingDisableAudio = 0xE8,
};
const char *builtInMethodToStr(BuiltInMethod method);
@@ -341,7 +348,9 @@ enum EventType {
kMovieAbortEvent = 0x15,
kMovieFailureEvent = 0x16,
kSpriteMovieEndEvent = 0x17,
- kDotGameCompleteEvent = 0x18,
+ kMinigameSuccessEvent = 0x18,
+ kMinigameDefeatEvent = 0x19,
+ kMinigameCrouchedEvent = 0x1A,
kScreenExitEvent = 0x1B,
kPathStepEvent = 0x1C,
kSoundStoppedEvent = 0x1D,
diff --git a/engines/mediastation/minigames/dotgame.cpp b/engines/mediastation/minigames/dotgame.cpp
index 98aaa285491..bd2470381e0 100644
--- a/engines/mediastation/minigames/dotgame.cpp
+++ b/engines/mediastation/minigames/dotgame.cpp
@@ -76,14 +76,14 @@ void DotGameActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType)
ScriptValue DotGameActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
ScriptValue returnValue;
switch (methodId) {
- case kDotGameResetMethod: {
+ case kMinigameResetMethod: {
ARGCOUNTCHECK(1);
int16 targetDot = static_cast<int16>(args[0].asFloat());
doReset(targetDot);
break;
}
- case kDotGameShowMethod:
+ case kMinigameActivateMethod:
ARGCOUNTCHECK(0);
if (!_isVisible) {
_isVisible = true;
@@ -93,7 +93,7 @@ ScriptValue DotGameActor::callMethod(BuiltInMethod methodId, Common::Array<Scrip
}
break;
- case kDotGameHideMethod:
+ case kMinigameDeactivateMethod:
ARGCOUNTCHECK(0);
if (_isVisible) {
_isVisible = false;
@@ -259,10 +259,10 @@ void DotGameActor::doHit() {
bool allDotsCompleted = (_totalDots - 1 == _currentDotIndex);
if (allDotsCompleted) {
- callMethod(kDotGameHideMethod, emptyArgs);
+ callMethod(kMinigameDeactivateMethod, emptyArgs);
_currentDotIndex = 0;
_animationProgress = 100;
- runScriptResponseIfExists(kDotGameCompleteEvent);
+ runScriptResponseIfExists(kMinigameSuccessEvent);
} else {
updateHelpers();
activateHelpers();
@@ -328,7 +328,7 @@ void DotGameActor::loadIsComplete() {
if (_isVisible) {
_isVisible = false;
Common::Array<ScriptValue> emptyArgs;
- callMethod(kDotGameHideMethod, emptyArgs);
+ callMethod(kMinigameDeactivateMethod, emptyArgs);
}
SpatialEntity::loadIsComplete();
diff --git a/engines/mediastation/minigames/stalkingzazu.cpp b/engines/mediastation/minigames/stalkingzazu.cpp
new file mode 100644
index 00000000000..761f78a97f3
--- /dev/null
+++ b/engines/mediastation/minigames/stalkingzazu.cpp
@@ -0,0 +1,1278 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "math/utils.h"
+#include "graphics/paletteman.h"
+
+#include "mediastation/minigames/stalkingzazu.h"
+#include "mediastation/actors/sprite.h"
+#include "mediastation/actors/sound.h"
+#include "mediastation/datafile.h"
+#include "mediastation/debugchannels.h"
+#include "mediastation/mediastation.h"
+
+namespace MediaStation {
+
+namespace StalkingZazuMinigame {
+
+Direction::Direction(uint directionToken) {
+ switch(directionToken) {
+ case 0x44c:
+ _value = Direction::kWest;
+ break;
+
+ case 0x44d:
+ _value = Direction::kSouthWest;
+ break;
+
+ case 0x44e:
+ _value = Direction::kSouth;
+ break;
+
+ case 0x44f:
+ _value = Direction::kSouthEast;
+ break;
+
+ case 0x450:
+ _value = Direction::kEast;
+ break;
+
+ case 0x451:
+ _value = Direction::kNorthEast;
+ break;
+
+ case 0x452:
+ _value = Direction::kNorth;
+ break;
+
+ case 0x453:
+ _value = Direction::kNorthWest;
+ break;
+
+ default:
+ error("%s: Got bad direction token %d", __func__, directionToken);
+ }
+}
+
+StalkingZazuMinigame::Direction StalkingZazuMinigame::Direction::rotate(uint steps) const {
+ uint zeroBased = (_value - 1 + steps) % Direction::TOTAL_DIRECTIONS;
+ return static_cast<Value>(zeroBased + 1);
+}
+
+uint StalkingZazuMinigame::StalkClips::indexOf(Direction direction, bool isSimbaCrouching) {
+ uint index = static_cast<uint>(direction);
+ if (isSimbaCrouching) {
+ // The clips are read in such that, for one direction, the crouched clip is always
+ // this far away from the uncrouched clip.
+ index += 9;
+ }
+ return index;
+}
+
+CellCoord CellCoord::getAdjacentCell(const Direction &direction) const {
+ // Compute the neighbour purely arithmetically. Whether the result lands on
+ // the grid is the field's concern, so this must be validated before use.
+ CellCoord adjacentCell(x, y);
+
+ switch (direction) {
+ case Direction::kEast:
+ adjacentCell.x = adjacentCell.x + 1;
+ break;
+
+ case Direction::kNorthEast:
+ adjacentCell.y = adjacentCell.y - 1;
+ adjacentCell.x = adjacentCell.x + 1;
+ break;
+
+ case Direction::kNorth:
+ adjacentCell.y = adjacentCell.y - 1;
+ break;
+
+ case Direction::kNorthWest:
+ adjacentCell.y = adjacentCell.y - 1;
+ adjacentCell.x = adjacentCell.x - 1;
+ break;
+
+ case Direction::kWest:
+ adjacentCell.x = adjacentCell.x - 1;
+ break;
+
+ case Direction::kSouthWest:
+ adjacentCell.x = adjacentCell.x - 1;
+ adjacentCell.y = adjacentCell.y + 1;
+ break;
+
+ case Direction::kSouth:
+ adjacentCell.y = adjacentCell.y + 1;
+ break;
+
+ case Direction::kSouthEast:
+ adjacentCell.y = adjacentCell.y + 1;
+ adjacentCell.x = adjacentCell.x + 1;
+ break;
+
+ default:
+ error("%s: Got bad direction token %d", __func__, static_cast<uint>(direction));
+ }
+
+ return adjacentCell;
+}
+
+StalkingField::StalkingField(int rowCount, int columnCount, const Common::Point &graphicalSize) {
+ _graphicalSize = graphicalSize;
+ _columns = columnCount;
+ _rows = rowCount;
+ _values.resize(rowCount * columnCount);
+}
+
+uint16 &StalkingField::at(int row, int column) {
+ assert(row < _rows);
+ assert(column < _columns);
+ return _values[(row * _columns) + column];
+}
+
+const uint16 &StalkingField::at(int row, int column) const {
+ assert(row < _rows);
+ assert(column < _columns);
+ return _values[(row * _columns) + column];
+}
+
+uint16 &StalkingField::at(const CellCoord &coord) {
+ assert(coord.y >= 0);
+ assert(coord.x >= 0);
+ return at(coord.y, coord.x);
+}
+
+const uint16 &StalkingField::at(const CellCoord &coord) const {
+ assert(coord.y >= 0);
+ assert(coord.x >= 0);
+ return at(coord.y, coord.x);
+}
+
+bool StalkingField::isInBounds(int row, int column) const {
+ return (row >= 0) && (column >= 0) &&
+ (row < _rows) && (column < _columns);
+}
+
+bool StalkingField::isInBounds(const CellCoord &coord) const {
+ return isInBounds(coord.y, coord.x);
+}
+
+void StalkingField::clear() {
+ Common::fill(_values.begin(), _values.end(), 0);
+}
+
+Common::Point StalkingField::bottomCenterPositionInFieldCoordinates() {
+ Common::Point bottomCenterPosition = Common::Point(
+ _graphicalSize.x / 2.0,
+ _graphicalSize.y - (cellSize().y / 2.0));
+
+ debugC(5, kDebugMinigame, "%s: (%d, %d) [graphicalSize: (%d, %d); cellSize: (%d, %d)]",
+ __func__, bottomCenterPosition.x, bottomCenterPosition.y,
+ _graphicalSize.x, _graphicalSize.y,
+ cellSize().x, cellSize().y);
+
+ return bottomCenterPosition;
+}
+
+CellCoord StalkingField::cellAtPoint(const Common::Point &pointInGraphicalCoordinates) {
+ // Get the logical cell at the given graphical point.
+ if ((_columns == 0) || (_rows == 0) || (_graphicalSize.x == 0) || (_graphicalSize.y == 0)) {
+ error("%s: Attempted to get cell in empty field", __func__);
+ }
+
+ const int16 column = MIN<int16>(pointInGraphicalCoordinates.x * _columns / _graphicalSize.x, _columns - 1);
+ const int16 row = MIN<int16>(pointInGraphicalCoordinates.y * _rows / _graphicalSize.y, _rows - 1);
+ return CellCoord(column, row);
+}
+
+Common::Point StalkingField::cellSize() {
+ // Get the size of each cell in graphical units.
+ return Common::Point(
+ static_cast<int16>(_graphicalSize.x / (double)_columns),
+ static_cast<int16>(_graphicalSize.y / (double)_rows));
+}
+
+Common::Point StalkingField::centerOfCell(const CellCoord &coord) {
+ // Get the center of each cell in graphical units.
+ Common::Point size = cellSize();
+ return Common::Point(
+ static_cast<int16>(coord.x * size.x + size.x / 2.0),
+ static_cast<int16>(coord.y * size.y + size.y / 2.0));
+}
+
+bool StalkingField::obstacleCanBePlaced(const CellCoord &coord) {
+ Direction direction(Direction::kNone);
+ Common::Array<CellCoord> scratch;
+ return obstacleCanBePlaced(coord, direction, scratch);
+}
+
+bool StalkingField::obstacleCanBePlaced(CellCoord currentPos, Direction direction, Common::Array<CellCoord> &visitedCells) {
+ // Don't allow placing an obstacle along any border.
+ bool canPlace = true;
+ visitedCells.push_back(currentPos);
+ if ((currentPos.y == 0) || (currentPos.x == 0) || \
+ (currentPos.y == _rows - 1) || \
+ (currentPos.x == _columns - 1)) {
+ return false;
+ }
+
+ // Don't allow placing an obstacle directly above or below an existing one.
+ if ((direction == Direction::kSouth) || (direction == Direction::kNorth)) {
+ return false;
+ }
+
+ Direction startDirection;
+ Direction stopDirection;
+ if (direction == Direction::kNone) {
+ startDirection = Direction::kEast;
+ stopDirection = Direction::kEast;
+ } else {
+ startDirection = direction.counterClockwise();
+ stopDirection = direction;
+ }
+
+ Direction currentDirection = startDirection;
+ Direction rotatedDirection;
+ do {
+ CellCoord adjacentCell = currentPos.getAdjacentCell(currentDirection);
+ if (isInBounds(adjacentCell)) {
+ if (!isEmpty(adjacentCell)) {
+ bool adjacentCellWasVisited = false;
+ // TODO: Can we use a hashmap here?
+ for (const CellCoord &visitedCell : visitedCells) {
+ if (visitedCell == adjacentCell) {
+ adjacentCellWasVisited = true;
+ break;
+ }
+ }
+
+ if (!adjacentCellWasVisited) {
+ const Direction reverseDirection = currentDirection.opposite();
+ canPlace = obstacleCanBePlaced(adjacentCell, reverseDirection, visitedCells);
+ }
+ }
+ }
+
+ rotatedDirection = currentDirection.counterClockwise();
+ currentDirection = rotatedDirection;
+ if (rotatedDirection == stopDirection) {
+ break;
+ }
+ } while (canPlace);
+ return canPlace;
+}
+
+
+void StalkingField::placeObstacles(const Common::Array<uint> &obstacleActorIds) {
+ // Randomly partition the obstacles across the playable rows.
+ // Each partition boundary marks the end of the current row's allotment of obstacles.
+ // Each row receives a consecutive range of obstacles while randomly varying
+ // how many obstacles each row contains.
+ Common::Array<uint16> partitionBoundaryIndices = calcPartitionBoundaries(_rows - 2, obstacleActorIds.size());
+ uint obstacleIndex = 0;
+ uint row = 1;
+
+ for (uint16 partitionBoundaryIndex : partitionBoundaryIndices) {
+ while (obstacleIndex < partitionBoundaryIndex) {
+ bool obstacleAdded = addObstacleToRow(static_cast<uint16>(obstacleActorIds[obstacleIndex]), row);
+ if (!obstacleAdded) {
+ // Give up on this row.
+ break;
+ }
+
+ obstacleIndex++;
+ }
+ row++;
+ }
+}
+
+Common::Array<uint16> StalkingField::calcPartitionBoundaries(uint totalRows, uint totalObstacles) {
+ // Generate random cumulative partition boundaries for an ordered list.
+ Common::Array<uint16> boundaryIndices;
+ for (uint i = 0; i < totalRows; i++) {
+ uint16 breakPoint = static_cast<uint16>(g_engine->_randomSource.getRandomNumber(totalObstacles));
+ boundaryIndices.push_back(breakPoint);
+ }
+
+ Common::sort(boundaryIndices.begin(), boundaryIndices.end());
+ return boundaryIndices;
+}
+
+
+bool StalkingField::addObstacleToRow(uint16 obstacleActorId, uint16 row) {
+ Common::Array<CellCoord> availableCellsOnRow = calcAvailableCellsOnRow(row);
+
+ bool obstacleAdded = false;
+ while (availableCellsOnRow.size() > 0) {
+ uint16 index = g_engine->_randomSource.getRandomNumber(availableCellsOnRow.size() - 1);
+ CellCoord &coord = availableCellsOnRow[index];
+ if (obstacleCanBePlaced(coord)) {
+ at(coord) = obstacleActorId;
+ obstacleAdded = true;
+ break;
+ } else {
+ availableCellsOnRow.remove_at(index);
+ }
+ }
+
+ return obstacleAdded;
+}
+
+Common::Array<CellCoord> StalkingField::calcAvailableCellsOnRow(uint row) {
+ Common::Array<CellCoord> cells;
+ for (int column = 0; column < _columns; ++column) {
+ CellCoord cellCoord(column, row);
+ if (isEmpty(cellCoord)) {
+ cells.push_back(cellCoord);
+ }
+ }
+ return cells;
+}
+
+void GameStateMachine::executeNextState(GameStateEvent event) {
+ switch (event) {
+ case kStalkingZazuResetStateEvent:
+ switch (_currentState) {
+ case kStalkingZazuStateStart:
+ case kStalkingZazuStateCrouched:
+ case kStalkingZazuStateCanMove:
+ case kStalkingZazuStateProcessMove:
+ case kStalkingZazuStateDefeat:
+ case kStalkingZazuStateSuccess:
+ _currentState = kStalkingZazuStateReset;
+ _actor->stalkingState_1_reset();
+ break;
+
+ default:
+ warnOnInvalidTransition(event);
+ }
+ break;
+
+ case kStalkingZazuCrouchStateEvent:
+ switch (_currentState) {
+ case kStalkingZazuStateReset:
+ case kStalkingZazuStateCanMove:
+ case kStalkingZazuStateProcessMove:
+ _currentState = kStalkingZazuStateCrouched;
+ _actor->stalkingState_2_crouched();
+ break;
+
+ default:
+ warnOnInvalidTransition(event);
+ }
+ break;
+
+ case kStalkingZazuUncrouchStateEvent:
+ switch (_currentState) {
+ case kStalkingZazuStateCrouched:
+ _currentState = kStalkingZazuStateCanMove;
+ _actor->stalkingState_3_canMove();
+ break;
+
+ default:
+ warnOnInvalidTransition(event);
+ }
+ break;
+
+ case kStalkingZazuMoveStateEvent:
+ switch (_currentState) {
+ case kStalkingZazuStateCanMove:
+ case kStalkingZazuStateProcessMove:
+ _currentState = kStalkingZazuStateProcessMove;
+ _actor->stalkingState_4_processMove();
+ break;
+
+ default:
+ warnOnInvalidTransition(event);
+ }
+ break;
+
+ case kStalkingZazuLoseStateEvent:
+ switch (_currentState) {
+ case kStalkingZazuStateCanMove:
+ case kStalkingZazuStateProcessMove:
+ _currentState = kStalkingZazuStateDefeat;
+ _actor->stalkingState_5_defeat();
+ break;
+
+ default:
+ warnOnInvalidTransition(event);
+ }
+ break;
+
+ case kStalkingZazuWinStateEvent:
+ switch (_currentState) {
+ case kStalkingZazuStateCanMove:
+ case kStalkingZazuStateProcessMove:
+ _currentState = kStalkingZazuStateSuccess;
+ _actor->stalkingState_6_success();
+ break;
+
+ default:
+ warnOnInvalidTransition(event);
+ }
+ break;
+
+ default:
+ warnOnInvalidTransition(event);
+ }
+}
+
+void TimerStateMachine::executeNextState(TimerEvent eventType) {
+ switch (eventType) {
+ case kStalkingZazuActivateTimerEvent:
+ switch (_currentState) {
+ case kStalkingZazuExpiredInactiveTimerState:
+ _currentState = kStalkingZazuExpiredActiveTimerState;
+ // This state doesn't have a callback.
+ break;
+
+ case kStalkingZazuQueuedInactiveTimerState:
+ _currentState = kStalkingZazuQueuedTimerState;
+ _actor->timerState_1_queued();
+ break;
+
+ default:
+ warnOnInvalidTransition(eventType);
+ }
+ break;
+
+ case kStalkingZazuInactivateTimerEvent:
+ switch (_currentState) {
+ case kStalkingZazuExpiredTimerState:
+ case kStalkingZazuExpiredActiveTimerState:
+ _currentState = kStalkingZazuExpiredInactiveTimerState;
+ // This state doesn't have a callback.
+ break;
+
+ case kStalkingZazuQueuedTimerState:
+ _currentState = kStalkingZazuQueuedInactiveTimerState;
+ // This state doesn't have a callback.
+ break;
+
+ default:
+ warnOnInvalidTransition(eventType);
+ }
+ break;
+
+ case kStalkingZazuExpireTimerEvent:
+ switch (_currentState) {
+ case kStalkingZazuQueuedTimerState:
+ _currentState = kStalkingZazuExpiredTimerState;
+ _actor->timerState_0_expired();
+ break;
+
+ default:
+ warnOnInvalidTransition(eventType);
+ }
+ break;
+
+ case kStalkingZazuQueueTimerEvent:
+ switch (_currentState) {
+ case kStalkingZazuExpiredTimerState:
+ case kStalkingZazuExpiredActiveTimerState:
+ _currentState = kStalkingZazuQueuedTimerState;
+ _actor->timerState_1_queued();
+ break;
+
+ case kStalkingZazuExpiredInactiveTimerState:
+ _currentState = kStalkingZazuQueuedInactiveTimerState;
+ // This state doesn't have a callback.
+ break;
+
+ default:
+ warnOnInvalidTransition(eventType);
+ }
+ break;
+
+ default:
+ warnOnInvalidTransition(eventType);
+ }
+}
+
+} // End of namespace StalkingZazuMinigame
+
+using namespace StalkingZazuMinigame;
+void StalkingZazuActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
+ switch (paramType) {
+ case kActorHeaderCursorResourceId:
+ _cursorResourceId = chunk.readTypedUint16();
+ break;
+
+ case kActorHeaderFrameRate: {
+ double timerFiresPerSecond = chunk.readTypedDouble();
+ _timerDurationInSeconds = 1.0 / timerFiresPerSecond;
+ break;
+ }
+
+ case kActorHeaderEditable:
+ _isActive = static_cast<bool>(chunk.readTypedByte());
+ break;
+
+ case kActorHeaderStalkingZazuSpriteId:
+ _simbaActorId = chunk.readTypedUint16();
+ break;
+
+ case kActorHeaderStalkingZazuDirections:
+ _stalkClips.resize(StalkClips::SIZE);
+ for (uint i = 0; i < 5; i++) {
+ uint directionToken = chunk.readTypedUint16();
+ Direction direction(directionToken);
+
+ uint nonCrouchedClipId = chunk.readTypedUint16();
+ uint nonCrouchedClipIndex = _stalkClips.indexOf(direction, false);
+ _stalkClips[nonCrouchedClipIndex] = nonCrouchedClipId;
+
+ uint crouchedClipId = chunk.readTypedUint16();
+ uint crouchedClipIndex = _stalkClips.indexOf(direction, true);
+ _stalkClips[crouchedClipIndex] = crouchedClipId;
+ }
+ break;
+
+ case kActorHeaderStalkingZazuField: {
+ uint rowCount = chunk.readTypedUint16();
+ uint columnCount = chunk.readTypedUint16();
+ Common::Point graphicalSize = Common::Point(getBbox().width(), getBbox().height());
+ _stalkingField = StalkingField(rowCount, columnCount, graphicalSize);
+ _targetPosInFieldCoordinates = Common::Point(graphicalSize.x / 2, 0);
+ break;
+ }
+
+ case kActorHeaderStalkingZazuObstacleActorId: {
+ uint obstacleActorId = chunk.readTypedUint16();
+ _obstacleActorIds.push_back(obstacleActorId);
+ break;
+ }
+
+ case kActorHeaderStalkingZazuUnkX:
+ _scalingFactor = chunk.readTypedGraphicUnit();
+ break;
+
+ case kActorHeaderStalkingZazuUnkY:
+ _successRadius = chunk.readTypedGraphicUnit();
+ break;
+
+ case kActorHeaderStalkingZazuUnkSound1:
+ _soundEffectId1 = chunk.readTypedUint16();
+ break;
+
+ case kActorHeaderStalkingZazuUnkSound2:
+ _soundEffectId2 = chunk.readTypedUint16();
+ break;
+
+ case kActorHeaderStalkingZazuAudioEnabled:
+ _soundEffectsEnabled = static_cast<bool>(chunk.readTypedByte());
+ break;
+
+ case kActorHeaderStalkingZazuObstaclesToShow:
+ _obstaclesToShow = chunk.readTypedSint16();
+ break;
+
+ case kActorHeaderStalkingZazuUnkPoint:
+ // This is read into offset 0xDE in the original
+ // but doesn't seem to ever be accessed, so we will throw it away.
+ chunk.readTypedPoint();
+ break;
+
+ default:
+ SpatialEntity::readParameter(chunk, paramType);
+ }
+}
+
+ScriptValue StalkingZazuActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
+ ScriptValue returnValue;
+ switch (methodId) {
+ case kStalkingStartZazuLookingMethod:
+ ARGCOUNTCHECK(0);
+ setZazuLook(true);
+ break;
+
+ case kStalkingStopZazuLookingMethod:
+ ARGCOUNTCHECK(0);
+ setZazuLook(false);
+ break;
+
+ case kStalkingGetZazuLookDirectionMethod: {
+ ARGCOUNTCHECK(0);
+ switch (directionZazuWillLook()) {
+ case Direction::kEast:
+ returnValue.setToParamToken(0x450);
+ break;
+
+ case Direction::kWest:
+ returnValue.setToParamToken(0x44c);
+ break;
+
+ default:
+ returnValue.setToParamToken(0x450);
+ }
+ break;
+ }
+
+ case kMinigameResetMethod:
+ ARGCOUNTCHECK(1);
+ _obstaclesToShow = static_cast<int>(args[0].asFloat());
+ generateStalkingEvent(kStalkingZazuResetStateEvent);
+ _gameMachine.runIfNotNested();
+ break;
+
+ case kMinigameActivateMethod:
+ ARGCOUNTCHECK(0);
+ setActive(true);
+ break;
+
+ case kMinigameDeactivateMethod:
+ ARGCOUNTCHECK(0);
+ setActive(false);
+ break;
+
+ case kStalkingEnableAudio:
+ ARGCOUNTCHECK(0);
+ setAudio(true);
+ break;
+
+ case kStalkingDisableAudio:
+ ARGCOUNTCHECK(0);
+ setAudio(false);
+ break;
+
+ default:
+ returnValue = SpatialEntity::callMethod(methodId, args);
+ }
+
+ return returnValue;
+}
+
+bool StalkingZazuActor::isVisible() const {
+ // This actor doesn't actually draw anything by itself. It controls other actors
+ // to show the user the game. However, drawing the stalking grid and such is extremely
+ // useful for debugging. But we only want to draw this grid if debugging is enabled.
+ return debugChannelSet(4, kDebugMinigame);
+}
+
+void StalkingZazuActor::draw(DisplayContext &displayContext) {
+ Graphics::ManagedSurface *targetSurface = displayContext._destImage;
+ if (targetSurface == nullptr) {
+ warning("[%s] %s: No target surface to draw", debugName(), __func__);
+ return;
+ }
+
+ const int rowCount = _stalkingField.rowCount();
+ const int columnCount = _stalkingField.columnCount();
+ if (rowCount == 0 || columnCount == 0) {
+ return;
+ }
+
+ // Outline each cell boundary and the center of each cell to visualize the stalking field.
+ const Common::Point drawOrigin = getBbox().origin() + displayContext._origin;
+ const int16 markerRadius = 4;
+ const Graphics::Palette currentPalette = g_system->getPaletteManager()->grabPalette(0, Graphics::PALETTE_COUNT);
+ const byte blackPaletteIndex = currentPalette.findBestColor(0, 0, 0);
+ const byte greenPaletteIndex = currentPalette.findBestColor(0, 255, 0);
+ const byte redPaletteIndex = currentPalette.findBestColor(255, 0, 0);
+ for (int row = 0; row < rowCount; ++row) {
+ for (int column = 0; column < columnCount; ++column) {
+ Common::Rect cellBounds(
+ (static_cast<int32>(column) * getBbox().width()) / columnCount,
+ (static_cast<int32>(row) * getBbox().height()) / rowCount,
+ (static_cast<int32>(column + 1) * getBbox().width()) / columnCount,
+ (static_cast<int32>(row + 1) * getBbox().height()) / rowCount
+ );
+ cellBounds.translate(drawOrigin.x, drawOrigin.y);
+ const bool cellContainsObstacle = _stalkingField.at(row, column) != 0;
+ const byte cellOutlinePaletteIndex = cellContainsObstacle ? redPaletteIndex : blackPaletteIndex;
+ targetSurface->frameRect(cellBounds, cellOutlinePaletteIndex);
+
+ Common::Point cellBoundsCenterInScreenCoordinates = cellBounds.center();
+ targetSurface->fillRect(
+ Common::Rect::center(
+ cellBoundsCenterInScreenCoordinates.x, cellBoundsCenterInScreenCoordinates.y,
+ markerRadius, markerRadius),
+ redPaletteIndex);
+ }
+ }
+
+ // Outline the area where Simba is allowed to move.
+ Common::Rect movementBoundsOnScreen = _simbaMovementBoundsInFieldCoordinates;
+ movementBoundsOnScreen.translate(drawOrigin.x, drawOrigin.y);
+ targetSurface->frameRect(movementBoundsOnScreen, greenPaletteIndex);
+
+ // Outline the bottom center position (Simba's starting point).
+ const Common::Point bottomCenterPositionInFieldCoordinates = _stalkingField.bottomCenterPositionInFieldCoordinates();
+ const Common::Point markerCenter = drawOrigin + bottomCenterPositionInFieldCoordinates;
+ targetSurface->fillRect(
+ Common::Rect::center(markerCenter.x, markerCenter.y, markerRadius, markerRadius),
+ redPaletteIndex);
+
+ // Draw the target point (Simba's ending point).
+ const Common::Point targetMarkerCenter = drawOrigin + _targetPosInFieldCoordinates;
+ targetSurface->fillRect(
+ Common::Rect::center(targetMarkerCenter.x, targetMarkerCenter.y, markerRadius, markerRadius),
+ redPaletteIndex);
+}
+
+void StalkingZazuActor::loadIsComplete() {
+ SpatialEntity::loadIsComplete();
+
+ // Shrink the movement bounds by half Simba's width. This is to keep Simba's
+ // center from reaching the center rather than its left edge.
+ _simbaMovementBoundsInFieldCoordinates = Common::Rect(0, 0, getBbox().width(), getBbox().height());
+ SpriteMovieActor *simbaActor = static_cast<SpriteMovieActor *>(g_engine->getImtGod()->getActorByIdAndType(_simbaActorId, kActorTypeSprite));
+ if (simbaActor != nullptr) {
+ // This was in the original to handle the case where the movement bounds are smaller than
+ // the simba's width after the inset. Since this actor is only used for one minigame with
+ // constant dimensions, we don't really need this code, but it is kept in here for robustness.
+ const int16 halfSimbaWidth = static_cast<int16>(simbaActor->getBbox().width() / 2.0);
+ _simbaMovementBoundsInFieldCoordinates.left += halfSimbaWidth;
+ _simbaMovementBoundsInFieldCoordinates.right -= halfSimbaWidth;
+ if (_simbaMovementBoundsInFieldCoordinates.left > _simbaMovementBoundsInFieldCoordinates.right) {
+ const int16 centerX = (_simbaMovementBoundsInFieldCoordinates.left + _simbaMovementBoundsInFieldCoordinates.right) / 2;
+ _simbaMovementBoundsInFieldCoordinates.left = centerX;
+ _simbaMovementBoundsInFieldCoordinates.right = centerX;
+ }
+ }
+
+ if (_obstaclesToShow != 0) {
+ generateStalkingEvent(kStalkingZazuResetStateEvent);
+ _gameMachine.runIfNotNested();
+ }
+}
+
+void StalkingZazuActor::onEvent(const ActorEvent &event) {
+ switch (event.type) {
+ case kMinigameSuccessEvent:
+ case kMinigameDefeatEvent:
+ case kMinigameCrouchedEvent:
+ runScriptResponseIfExists(event.type);
+ break;
+
+ default:
+ Actor::onEvent(event);
+ }
+}
+
+void StalkingZazuActor::timerEvent(const MediaStation::TimerEvent &event) {
+ generateTimerEvent(kStalkingZazuExpireTimerEvent);
+ _timerMachine.runIfNotNested();
+}
+
+void StalkingZazuActor::setZazuLook(bool zazuIsLooking) {
+ if (_zazuIsLooking != zazuIsLooking) {
+ _zazuIsLooking = zazuIsLooking;
+ if (zazuSeesSimba()) {
+ generateStalkingEvent(kStalkingZazuLoseStateEvent);
+ _gameMachine.runIfNotNested();
+ }
+ }
+}
+
+void StalkingZazuActor::setAudio(bool isEnabled) {
+ _soundEffectsEnabled = isEnabled;
+ if (!isEnabled) {
+ stopSound(_soundEffectId1);
+ stopSound(_soundEffectId2);
+ }
+}
+
+void StalkingZazuActor::startSound(uint soundActorId) {
+ if (soundActorId != 0) {
+ SoundActor *sound = static_cast<SoundActor *>(g_engine->getImtGod()->getActorByIdAndType(soundActorId, kActorTypeSound));
+ if (sound != nullptr) {
+ sound->start();
+ } else {
+ warning("[%s] %s: Null sound %d", debugName(), __func__, soundActorId);
+ }
+ }
+}
+
+void StalkingZazuActor::stopSound(uint soundActorId) {
+ if (soundActorId != 0) {
+ SoundActor *sound = static_cast<SoundActor *>(g_engine->getImtGod()->getActorByIdAndType(soundActorId, kActorTypeSound));
+ if (sound != nullptr) {
+ sound->stop();
+ } else {
+ warning("[%s] %s: Null sound %d", debugName(), __func__, soundActorId);
+ }
+ }
+}
+
+void StalkingZazuActor::setActive(bool isActive) {
+ if (_isActive != isActive) {
+ _isActive = isActive;
+ if (_isActive) {
+ generateTimerEvent(kStalkingZazuActivateTimerEvent);
+ } else {
+ _mouseIsInside = false;
+ _mouseIsDown = false;
+ generateTimerEvent(kStalkingZazuInactivateTimerEvent);
+ }
+ _timerMachine.runIfNotNested();
+
+ g_engine->generateMouseUpdateEvent();
+ }
+}
+
+void StalkingZazuActor::generateStalkingEvent(GameStateEvent event) {
+ _gameMachine.queueEvent(event);
+}
+
+void StalkingZazuActor::generateTimerEvent(StalkingZazuMinigame::TimerEvent event) {
+ _timerMachine.queueEvent(event);
+}
+
+StalkingZazuMinigame::Direction StalkingZazuActor::directionZazuWillLook() {
+ Common::Point delta = _targetPosInFieldCoordinates - _simbaPosInFieldCoordinates;
+ const double angleRadians = atan2(delta.y, delta.x);
+ const double angle = Math::rad2deg<double>(angleRadians);
+
+ if (angle >= -90 && angle < 90) {
+ return Direction::kEast;
+ } else {
+ return Direction::kWest;
+ }
+}
+
+void StalkingZazuActor::calcWhichObstaclesToShow(uint16 obstaclesToShow) {
+ int obstaclesToRemove = _obstacleActorIds.size() - obstaclesToShow;
+ if (obstaclesToRemove > 0) {
+ // The original removes obstacles at evenly distributed intervals. This ensures
+ // the remaining obstacles are spread out rather than clustered. However, this
+ // code doesn't seem to be reached in the original because scripts never seem to
+ // request to show fewer obstacles.
+ warning("%s: Showing all %d obstacles", __func__, obstaclesToShow);
+ }
+}
+
+void StalkingZazuActor::calcObstaclePlacements() {
+ _stalkingField.clear();
+ _stalkingField.placeObstacles(_obstacleActorIds);
+}
+
+void StalkingZazuActor::placeObstaclesOnStage() {
+ for (int row = 0; row < _stalkingField.rowCount(); ++row) {
+ for (int column = 0; column < _stalkingField.columnCount(); ++column) {
+ const uint16 obstacleActorId = _stalkingField.at(row, column);
+ if (obstacleActorId != 0) {
+ const CellCoord cell(static_cast<int16>(column), static_cast<int16>(row));
+ placeAndShowActor(obstacleActorId, _stalkingField.centerOfCell(cell));
+ }
+ }
+ }
+}
+
+void StalkingZazuActor::positionSimbaStartOnStage() {
+ _simbaPosInFieldCoordinates = _stalkingField.bottomCenterPositionInFieldCoordinates();
+ // This magic constant is in the original; not sure exactly why such a small offset is needed.
+ _simbaPosInFieldCoordinates.x += 3;
+ placeAndShowActor(_simbaActorId, _simbaPosInFieldCoordinates);
+
+ const Direction startDirection(Direction::kNorth);
+ setSimbaClipFromDirection(startDirection);
+ setSimbaCrouchClip(true);
+}
+
+void StalkingZazuActor::placeAndShowActor(uint actorId, const Common::Point &destInFieldCoordinates) {
+ SpatialEntity *actorToPlace = g_engine->getImtGod()->getSpatialEntityById(actorId);
+ if (actorToPlace == nullptr) {
+ warning("[%s] %s: Got null actor %d", debugName(), __func__, actorId);
+ return;
+ }
+
+ uint zCoord = zCoordOfPoint(destInFieldCoordinates);
+ actorToPlace->setZIndex(zCoord);
+
+ // Convert field coordinates to screen coordinates and center the actor directly over
+ // the destInFieldCoordinates.
+ Common::Point destInScreenCoordinates = destInFieldCoordinates + getBbox().origin();
+ // We could use moveToCentered here, but the engine for this minigame didn't seem
+ // to have it and did the centering calculation manually. So to avoid confusion when comparing
+ // with disassembly, we will also do the centering calculation manually here.
+ destInScreenCoordinates.x -= actorToPlace->getBbox().width() / 2;
+ destInScreenCoordinates.y -= actorToPlace->getBbox().height() / 2;
+ actorToPlace->moveTo(destInScreenCoordinates.x, destInScreenCoordinates.y);
+
+ Common::Array<ScriptValue> emptyArgs(0);
+ actorToPlace->callMethod(kSpatialShowMethod, emptyArgs);
+}
+
+int StalkingZazuActor::zCoordOfPoint(const Common::Point &point) {
+ // Items lower in the field (higher y) should appear in front of items higher up.
+ return zIndex() - point.y;
+}
+
+bool StalkingZazuActor::simbaIsInSuccessPosition() {
+ Common::Point delta = _targetPosInFieldCoordinates - _simbaPosInFieldCoordinates;
+ const double distanceToTarget = Math::hypotenuse<double>(static_cast<double>(delta.x), static_cast<double>(delta.y));
+ return (distanceToTarget <= _successRadius);
+}
+
+void StalkingZazuActor::setSimbaCrouchClip(bool isCrouchClipSet) {
+ if (isCrouchClipSet != _crouchClipIsSet) {
+ uint index = _stalkClips.indexOf(_currentDirection, isCrouchClipSet);
+ setClipOfMovie(_simbaActorId, _stalkClips[index]);
+ _crouchClipIsSet = isCrouchClipSet;
+ }
+}
+
+void StalkingZazuActor::processMove(Common::Point mousePos) {
+ Common::Point targetPosInFieldCoordinates = mousePos - getBbox().origin();
+ debugC(5, kDebugMinigame, "[%s] %s: currentPos: (%d, %d); dest: (%d, %d)", debugName(), __func__,
+ _simbaPosInFieldCoordinates.x, _simbaPosInFieldCoordinates.y, targetPosInFieldCoordinates.x, targetPosInFieldCoordinates.y);
+
+ Common::Point newPosInFieldCoordinates;
+ Direction direction;
+ bool simbaMoved = getMoveInformation(targetPosInFieldCoordinates, newPosInFieldCoordinates, direction);
+ if (simbaMoved) {
+ setSimbaClipFromDirection(direction);
+ if (isClearAtPoint(newPosInFieldCoordinates)) {
+ placeAndShowActor(_simbaActorId, newPosInFieldCoordinates);
+ SpriteMovieActor *actor = static_cast<SpriteMovieActor *>(g_engine->getImtGod()->getActorByIdAndType(_simbaActorId, kActorTypeSprite));
+ if (actor != nullptr) {
+ actor->activateNextFrame();
+ }
+
+ _simbaPosInFieldCoordinates = newPosInFieldCoordinates;
+ }
+ }
+}
+
+bool StalkingZazuActor::getMoveInformation(const Common::Point &targetPosInFieldCoordinates, Common::Point &nextPosInFieldCoordinates, Direction &direction) {
+ if (targetPosInFieldCoordinates == _simbaPosInFieldCoordinates) {
+ return false;
+ }
+
+ const Common::Point delta = targetPosInFieldCoordinates - _simbaPosInFieldCoordinates;
+ direction = directionOfVector(delta);
+ nextPosInFieldCoordinates = desiredPositionFromVectorInFieldCoordinates(delta);
+ if (nextPosInFieldCoordinates == _simbaPosInFieldCoordinates) {
+ return false;
+ }
+
+ return true;
+}
+
+Direction StalkingZazuActor::directionOfVector(const Common::Point &dest) {
+ const double angleRadians = atan2(static_cast<double>(dest.y), static_cast<double>(dest.x));
+ const double angle = Math::rad2deg<double>(angleRadians);
+
+ // This actor cannot move south. Any southerly vector is treated as
+ // either East or West depending on its horizontal component.
+ if (angle >= 90.0) return Direction::kWest;
+ else if (angle >= -22.5) return Direction::kEast;
+ else if (angle >= -67.5) return Direction::kNorthEast;
+ else if (angle >=-112.5) return Direction::kNorth;
+ else if (angle >=-157.5) return Direction::kNorthWest;
+ else return Direction::kWest;
+}
+
+Common::Point StalkingZazuActor::desiredPositionFromVectorInFieldCoordinates(const Common::Point &delta) {
+ const double length = Math::hypotenuse<double>(static_cast<double>(delta.x), static_cast<double>(delta.y));
+ double scale = 0.0;
+ if (length != 0.0) {
+ scale = static_cast<double>(_scalingFactor) / length;
+ }
+
+ int16 dx = static_cast<int16>(round(delta.x * scale));
+ int16 dy = static_cast<int16>(round(delta.y * scale));
+
+ // Don't allow the normalized vector to overshoot the original.
+ if (dy < 0) {
+ dy = MAX(dy, delta.y);
+ } else {
+ // Simba isn't allowed to backtrack.
+ dy = 0;
+ }
+
+ if (dx > 0) {
+ dx = MIN(dx, delta.x);
+ } else {
+ dx = MAX(dx, delta.x);
+ }
+
+ Common::Point nextPosInFieldCoordinates(_simbaPosInFieldCoordinates.x + dx, _simbaPosInFieldCoordinates.y + dy);
+ debugC(4, kDebugMinigame, "[%s] %s: (%d, %d): (%d, %d) + (%d, %d) = (%d, %d)",
+ debugName(), __func__, delta.x, delta.y,
+ _simbaPosInFieldCoordinates.x, _simbaPosInFieldCoordinates.y, dx, dy,
+ nextPosInFieldCoordinates.x, nextPosInFieldCoordinates.y);
+
+ bool nextPosSuccessfullyConstrained = _simbaMovementBoundsInFieldCoordinates.constrain(nextPosInFieldCoordinates.x, nextPosInFieldCoordinates.y, 0, 0);
+ if (!nextPosSuccessfullyConstrained) {
+ warning("[%s] %s: Point constraint failed", debugName(), __func__);
+ }
+
+ return nextPosInFieldCoordinates;
+}
+
+void StalkingZazuActor::setSimbaClipFromDirection(Direction direction) {
+ if (_currentDirection != direction || _crouchClipIsSet) {
+ uint stalkClipIndex = _stalkClips.indexOf(direction, false);
+ setClipOfMovie(_simbaActorId, _stalkClips[stalkClipIndex]);
+ _crouchClipIsSet = false;
+ _currentDirection = direction;
+ debugC(5, kDebugMinigame, "[%s] %s: Simba is facing %d", debugName(), __func__, static_cast<uint>(direction));
+ }
+}
+
+void StalkingZazuActor::setClipOfMovie(uint spriteActorId, uint clipId) {
+ SpriteMovieActor *movie = static_cast<SpriteMovieActor *>(g_engine->getImtGod()->getActorByIdAndType(_simbaActorId, kActorTypeSprite));
+ if (movie != nullptr) {
+ movie->setCurrentClip(clipId);
+ }
+}
+
+bool StalkingZazuActor::isClearAtPoint(const Common::Point &posInFieldCoordinates) {
+ // Rather than testing every tile the rectangle overlaps, the code assumes the rectangle is
+ // at most one tile wide/high (here itâs half a tile), and walls occupy whole tiles. Under those
+ // assumptions, checking the four corners completely determines whether the rectangle intersects
+ // any blocked tile.
+ const Common::Point cellSize = _stalkingField.cellSize();
+ const int16 halfWidth = cellSize.x / 4;
+ const int16 halfHeight = cellSize.y / 4;
+
+ Common::Rect box(
+ Common::Point(posInFieldCoordinates.x - halfWidth, posInFieldCoordinates.y - halfHeight),
+ halfWidth * 2, halfHeight * 2);
+ box.clip(_simbaMovementBoundsInFieldCoordinates);
+
+ const int16 rightEdge = MAX<int16>(box.right - 1, box.left);
+ const int16 bottomEdge = MAX<int16>(box.bottom - 1, box.top);
+ const Common::Point corners[4] = {
+ Common::Point(box.left, box.top),
+ Common::Point(box.left, bottomEdge),
+ Common::Point(rightEdge, bottomEdge),
+ Common::Point(rightEdge, box.top)
+ };
+
+ for (const Common::Point &corner : corners) {
+ const CellCoord cell = _stalkingField.cellAtPoint(corner);
+ if (_stalkingField.at(cell) != 0) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void StalkingZazuActor::stalkingState_1_reset() {
+ _obstaclesToShow = MIN(_obstaclesToShow, (int)_obstacleActorIds.size());
+ if (_obstaclesToShow >= 0) {
+ calcWhichObstaclesToShow(_obstaclesToShow);
+ calcObstaclePlacements();
+ }
+
+ // hideObstacles
+ for (uint obstacleActorId : _obstacleActorIds) {
+ Actor *obstacle = g_engine->getImtGod()->getActorById(obstacleActorId);
+ if (obstacle != nullptr) {
+ Common::Array<ScriptValue> emptyArgs(0);
+ obstacle->callMethod(kSpatialHideMethod, emptyArgs);
+ }
+ }
+
+ placeObstaclesOnStage();
+ positionSimbaStartOnStage();
+ _simbaIsCrouching = false;
+ g_engine->generateMouseUpdateEvent();
+
+ setZazuLook(false);
+ _acceptingMouseEvents = true;
+}
+
+void StalkingZazuActor::stalkingState_2_crouched() {
+ if (!_simbaIsCrouching) {
+ _simbaIsCrouching = true;
+ g_engine->generateMouseUpdateEvent();
+
+ ActorEvent actorEvent(_id, kMinigameCrouchedEvent);
+ g_engine->getEventLoop()->queueEvent(actorEvent);
+ }
+
+ setSimbaCrouchClip(true);
+ if (_soundEffectsEnabled) {
+ startSound(_soundEffectId1);
+ }
+ _acceptingMouseEvents = false;
+}
+
+void StalkingZazuActor::stalkingState_3_canMove() {
+ setSimbaCrouchClip(false);
+ if (_soundEffectsEnabled) {
+ startSound(_soundEffectId1);
+ }
+
+ if (zazuSeesSimba()) {
+ generateStalkingEvent(kStalkingZazuLoseStateEvent);
+ _gameMachine.runIfNotNested();
+ _acceptingMouseEvents = false;
+ }
+
+ generateTimerEvent(kStalkingZazuQueueTimerEvent);
+ _timerMachine.runIfNotNested();
+}
+
+void StalkingZazuActor::stalkingState_4_processMove() {
+ if (zazuSeesSimba()) {
+ debugC(5, kDebugMinigame, "[%s] %s: LOSE", debugName(), __func__);
+ generateStalkingEvent(kStalkingZazuLoseStateEvent);
+ _gameMachine.runIfNotNested();
+
+ } else if (simbaIsInSuccessPosition()) {
+ debugC(5, kDebugMinigame, "[%s] %s: WIN", debugName(), __func__);
+ generateStalkingEvent(kStalkingZazuWinStateEvent);
+ _gameMachine.runIfNotNested();
+
+ } else {
+ Common::Point mousePos;
+ currentMousePosition(mousePos);
+ processMove(mousePos);
+ if (_soundEffectsEnabled) {
+ startSound(_soundEffectId2);
+ }
+
+ generateTimerEvent(kStalkingZazuQueueTimerEvent);
+ _timerMachine.runIfNotNested();
+ }
+}
+
+void StalkingZazuActor::stalkingState_5_defeat() {
+ ActorEvent actorEvent(_id, kMinigameDefeatEvent);
+ g_engine->getEventLoop()->queueEvent(actorEvent);
+}
+
+void StalkingZazuActor::stalkingState_6_success() {
+ ActorEvent actorEvent(_id, kMinigameSuccessEvent);
+ g_engine->getEventLoop()->queueEvent(actorEvent);
+}
+
+void StalkingZazuActor::timerState_0_expired() {
+ generateStalkingEvent(kStalkingZazuMoveStateEvent);
+ _gameMachine.runIfNotNested();
+}
+
+void StalkingZazuActor::timerState_1_queued() {
+ g_engine->getTimerService()->startTimer(_timer, _timerDurationInSeconds);
+}
+
+uint16 StalkingZazuActor::findActorToAcceptMouseEvents(
+ const Common::Point &point,
+ uint16 eventMask,
+ MouseActorState &state,
+ bool clipMouseEvents) {
+ (void)clipMouseEvents;
+
+ uint16 result = 0;
+ if (_isActive) {
+ const bool isMousePositionInteresting = mousePositionIsInteresting(point);
+ if (isMousePositionInteresting) {
+ if (eventMask & kMouseDownFlag) {
+ state.mouseDown = this;
+ result |= kMouseDownFlag;
+ }
+
+ if (eventMask & kMouseEnterFlag) {
+ state.mouseEnter = this;
+ result |= kMouseEnterFlag;
+ }
+
+ if (eventMask & kMouseMovedFlag) {
+ state.mouseMoved = this;
+ result |= kMouseMovedFlag;
+ }
+ }
+
+ if (_mouseIsInside && (eventMask & kMouseExitFlag)) {
+ state.mouseExit = this;
+ result |= kMouseExitFlag;
+ }
+
+ if (_mouseIsDown && (eventMask & kMouseUpFlag)) {
+ state.mouseUp = this;
+ result |= kMouseUpFlag;
+ }
+ }
+
+ return result;
+}
+
+bool StalkingZazuActor::mousePositionIsInteresting(const Common::Point &pos) {
+ bool isInteresting = true;
+ if (_acceptingMouseEvents) {
+ SpriteMovieActor *simba = static_cast<SpriteMovieActor *>(g_engine->getImtGod()->getActorByIdAndType(_simbaActorId, kActorTypeSprite));
+ if (simba == nullptr) {
+ isInteresting = false;
+ }
+ isInteresting = simba->getBbox().contains(pos);
+ }
+ return isInteresting;
+}
+
+void StalkingZazuActor::mouseDownEvent(const MouseEvent &event) {
+ _mouseIsDown = true;
+ generateStalkingEvent(kStalkingZazuCrouchStateEvent);
+ _gameMachine.runIfNotNested();
+}
+
+void StalkingZazuActor::mouseUpEvent(const MouseEvent &event) {
+ _mouseIsDown = false;
+ generateStalkingEvent(kStalkingZazuUncrouchStateEvent);
+ _gameMachine.runIfNotNested();
+}
+
+void StalkingZazuActor::mouseEnteredEvent(const MouseEvent &event) {
+ _mouseIsInside = true;
+ if (_cursorResourceId != 0) {
+ g_engine->getCursorManager()->setAsTemporary(_cursorResourceId);
+ } else {
+ g_engine->getCursorManager()->unsetTemporary();
+ }
+}
+
+void StalkingZazuActor::mouseExitedEvent(const MouseEvent &event) {
+ _mouseIsInside = false;
+}
+
+bool StalkingZazuActor::zazuSeesSimba() {
+ return _zazuIsLooking && !simbaIsBehindRock();
+}
+
+bool StalkingZazuActor::simbaIsBehindRock() {
+ // Simba is behind the rock iff the rock is directly
+ // north of the field cell he is currently in.
+ bool isBehindRock = false;
+ CellCoord currentCell = _stalkingField.cellAtPoint(_simbaPosInFieldCoordinates);
+
+ CellCoord cellNorthOfCurrent = currentCell.getAdjacentCell(Direction::kNorth);
+ if (_stalkingField.isInBounds(cellNorthOfCurrent)) {
+ uint16 cellValue = _stalkingField.at(cellNorthOfCurrent);
+ if (cellValue != 0) {
+ isBehindRock = true;
+ }
+ }
+
+ return isBehindRock;
+}
+
+Common::String StalkingZazuActor::debugString() const {
+ return Common::String::format("simbaActorId: %u\n", _simbaActorId);
+}
+
+} // End namespace MediaStation
diff --git a/engines/mediastation/minigames/stalkingzazu.h b/engines/mediastation/minigames/stalkingzazu.h
new file mode 100644
index 00000000000..9e3bb1d1fdf
--- /dev/null
+++ b/engines/mediastation/minigames/stalkingzazu.h
@@ -0,0 +1,288 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MEDIASTATION_MINIGAMES_STALKINGZAZU_H
+#define MEDIASTATION_MINIGAMES_STALKINGZAZU_H
+
+#include "mediastation/statemachine.h"
+#include "mediastation/actors/stage.h"
+
+namespace MediaStation {
+
+class StalkingZazuActor;
+namespace StalkingZazuMinigame {
+
+class Direction {
+public:
+ static const uint TOTAL_DIRECTIONS = 8;
+ enum Value {
+ kNone = 0,
+ kEast = 1,
+ kNorthEast = 2,
+ kNorth = 3,
+ kNorthWest = 4,
+ kWest = 5,
+ kSouthWest = 6,
+ kSouth = 7,
+ kSouthEast = 8
+ };
+
+ Direction() {}
+ Direction(Value value) : _value(value) {}
+ Direction(uint directionToken);
+
+ // Conversion operator so a Direction stands in for its enum value directly.
+ operator Value() const { return _value; }
+
+ Direction counterClockwise() const { return rotate(1); }
+ Direction opposite() const { return rotate(TOTAL_DIRECTIONS / 2); }
+
+private:
+ // Rotates counter-clockwise by the given number of eighth-turns,
+ Direction rotate(uint steps) const;
+ Value _value = kNone;
+};
+
+class CellCoord : public Common::Point {
+public:
+ CellCoord() : Common::Point() {}
+ CellCoord(int16 column, int16 row) : Common::Point(column, row) {}
+
+ CellCoord getAdjacentCell(const Direction &direction) const;
+};
+
+class StalkClips : public Common::Array<uint> {
+public:
+ static const uint SIZE = 18;
+ uint indexOf(Direction direction, bool isCrouched);
+};
+
+// This is basically a uint16 matrix with a lot of additional methods attached.
+// The ScummVM Matrix class was not used because it is float-backed and so not really
+// a great choice for an obstacle field. This is really a translation layer a logical
+// "stalking field" space that defines whether a given region of graphical space contains
+// an obstacle or not.
+class StalkingField {
+public:
+ StalkingField() = default;
+ StalkingField(int rowCount, int columnCount, const Common::Point &graphicalSize);
+
+ uint16 &at(int row, int column);
+ const uint16 &at(int row, int column) const;
+ uint16 &at(const CellCoord &coord);
+ const uint16 &at(const CellCoord &coord) const;
+ bool isEmpty(const CellCoord &coord) { return at(coord) == 0; }
+
+ bool isInBounds(int row, int column) const;
+ bool isInBounds(const CellCoord &coord) const;
+
+ int rowCount() const { return _rows; }
+ int columnCount() const { return _columns; }
+
+ void clear();
+ Common::Point bottomCenterPositionInFieldCoordinates();
+ CellCoord cellAtPoint(const Common::Point &point);
+ void placeObstacles(const Common::Array<uint> &obstacleActorIds);
+ Common::Point cellSize();
+ Common::Point centerOfCell(const CellCoord &coord);
+
+private:
+ bool addObstacleToRow(uint16 obstacleActorId, uint16 row);
+ Common::Array<CellCoord> calcAvailableCellsOnRow(uint row);
+ Common::Array<uint16> calcPartitionBoundaries(uint end, uint brk); // calcBreakPoints
+
+ bool obstacleCanBePlaced(const CellCoord &coord); // cellIsValidObstacle
+ bool obstacleCanBePlaced(CellCoord cellCoord, Direction direction, Common::Array<CellCoord> &cellCoordVector);
+
+ int _columns = 0;
+ int _rows = 0;
+ Common::Array<uint16> _values;
+ Common::Point _graphicalSize;
+};
+
+enum GameState {
+ kStalkingZazuStateStart = 0,
+ kStalkingZazuStateReset = 1,
+ kStalkingZazuStateCrouched = 2,
+ kStalkingZazuStateCanMove = 3,
+ kStalkingZazuStateProcessMove = 4,
+ kStalkingZazuStateDefeat = 5,
+ kStalkingZazuStateSuccess = 6
+};
+
+enum GameStateEvent {
+ kStalkingZazuResetStateEvent = 0,
+ kStalkingZazuCrouchStateEvent = 1,
+ kStalkingZazuUncrouchStateEvent = 2,
+ kStalkingZazuMoveStateEvent = 3,
+ kStalkingZazuLoseStateEvent = 4,
+ kStalkingZazuWinStateEvent = 5
+};
+
+struct GameStateMachineEvent : public StateMachineEvent<GameState> {
+public:
+ uint obstaclesToShow = 0;
+};
+
+class GameStateMachine : public StateMachine<GameState, GameStateEvent> {
+public:
+ GameStateMachine(StalkingZazuActor *actor) : _actor(actor) { _currentState = kStalkingZazuStateStart; }
+
+protected:
+ virtual void executeNextState(GameStateEvent eventType) override;
+ StalkingZazuActor *_actor = nullptr;
+};
+
+enum TimerState {
+ kStalkingZazuExpiredTimerState = 0,
+ kStalkingZazuQueuedTimerState = 1,
+ kStalkingZazuExpiredInactiveTimerState = 2,
+ kStalkingZazuExpiredActiveTimerState = 3,
+ kStalkingZazuQueuedInactiveTimerState = 4
+};
+
+enum TimerEvent {
+ kStalkingZazuActivateTimerEvent = 0,
+ kStalkingZazuInactivateTimerEvent = 1,
+ kStalkingZazuExpireTimerEvent = 2,
+ kStalkingZazuQueueTimerEvent = 3
+};
+
+class TimerStateMachine : public StateMachine<TimerState, TimerEvent> {
+public:
+ TimerStateMachine(StalkingZazuActor *actor) : _actor(actor) { _currentState = kStalkingZazuExpiredTimerState; }
+
+protected:
+ virtual void executeNextState(TimerEvent eventType) override;
+ StalkingZazuActor *_actor = nullptr;
+};
+
+}; // End of namespace StalkingZazuMinigame
+
+class StalkingZazuActor : public SpatialEntity {
+friend class StalkingZazuMinigame::GameStateMachine;
+friend class StalkingZazuMinigame::TimerStateMachine;
+
+public:
+ StalkingZazuActor() : SpatialEntity(kActorTypeStalkingZazu), _gameMachine(this), _timerMachine(this) {};
+
+ virtual Common::String debugString() const override;
+ virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
+ virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+ virtual bool isVisible() const override;
+ virtual void draw(DisplayContext &displayContext) override;
+ virtual void loadIsComplete() override;
+
+ virtual void onEvent(const ActorEvent &event) override;
+ virtual void timerEvent(const MediaStation::TimerEvent &event) override;
+
+private:
+ StalkingZazuMinigame::GameStateMachine _gameMachine;
+ void generateStalkingEvent(StalkingZazuMinigame::GameStateEvent event);
+ StalkingZazuMinigame::TimerStateMachine _timerMachine;
+ void generateTimerEvent(StalkingZazuMinigame::TimerEvent event);
+
+ void calcWhichObstaclesToShow(uint16 obstaclesToShow);
+ void calcObstaclePlacements();
+ void placeObstaclesOnStage();
+ void positionSimbaStartOnStage();
+
+ bool simbaIsInSuccessPosition();
+ void setSimbaCrouchClip(bool isSimbaCrouching);
+ void setSimbaClipFromDirection(StalkingZazuMinigame::Direction direction);
+ bool zazuSeesSimba();
+ bool simbaIsBehindRock();
+
+ void placeAndShowActor(uint obstacleActorId, const Common::Point &pos);
+ void setClipOfMovie(uint spriteActorId, uint clipId);
+ int zCoordOfPoint(const Common::Point &point);
+
+ void processMove(Common::Point dest);
+ bool getMoveInformation(const Common::Point &targetPos, Common::Point &nextPos, StalkingZazuMinigame::Direction &direction);
+ StalkingZazuMinigame::Direction directionOfVector(const Common::Point &dest);
+ Common::Point desiredPositionFromVectorInFieldCoordinates(const Common::Point &point);
+ bool isClearAtPoint(const Common::Point &point);
+
+ StalkingZazuMinigame::Direction directionZazuWillLook();
+ void setZazuLook(bool isLooking);
+ void setActive(bool isActive);
+
+ void stalkingState_1_reset();
+ void stalkingState_2_crouched();
+ void stalkingState_3_canMove();
+ void stalkingState_4_processMove();
+ void stalkingState_5_defeat();
+ void stalkingState_6_success();
+ void timerState_0_expired();
+ void timerState_1_queued();
+ // The original has a few other timer states that are no-ops,
+ // so they are excluded here.
+
+ virtual bool interactsWithMouse() const override { return _isActive; }
+ virtual uint16 findActorToAcceptMouseEvents(
+ const Common::Point &point,
+ uint16 eventMask,
+ MouseActorState &state,
+ bool clipMouseEvents) override;
+ bool mousePositionIsInteresting(const Common::Point &pos);
+ void mouseDownEvent(const MouseEvent &event) override;
+ void mouseUpEvent(const MouseEvent &event) override;
+ void mouseEnteredEvent(const MouseEvent &event) override;
+ void mouseExitedEvent(const MouseEvent &event) override;
+ bool _mouseIsDown = false;
+ bool _mouseIsInside = false;
+
+ StalkingZazuMinigame::StalkingField _stalkingField;
+ StalkingZazuMinigame::StalkClips _stalkClips;
+ Common::Point _targetPosInFieldCoordinates;
+ Common::Point _simbaPosInFieldCoordinates;
+ Common::Rect _simbaMovementBoundsInFieldCoordinates; // Offset 0x104 in original (FP68K calls obscure xrefs).
+ StalkingZazuMinigame::Direction _currentDirection;
+
+ uint _cursorResourceId = 0;
+ uint _simbaActorId = 0;
+ int _obstaclesToShow = 0;
+ Common::Array<uint> _obstacleActorIds;
+
+ bool _isActive = false;
+ bool _simbaIsCrouching = false;
+ bool _crouchClipIsSet = false;
+ bool _acceptingMouseEvents = true;
+ bool _zazuIsLooking = false;
+
+ double _timerDurationInSeconds = 0.10;
+ uint _scalingFactor = 10; // Offset 0xD8 in original (FP68K calls obscure xrefs).
+ uint _successRadius = 20; // Offset 0xFE in original (FP68K calls obscure xrefs).
+
+ // These are read in the original, but they don't seem to actually be populated
+ // and sound effects don't seem to be enabled. However, they are included in case
+ // they are used in some localization.
+ bool _soundEffectsEnabled = false;
+ uint _soundEffectId1 = 0;
+ uint _soundEffectId2 = 0;
+ void setAudio(bool isEnabled);
+ void startSound(uint soundActorId);
+ void stopSound(uint soundActorId);
+};
+
+} // End namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 3352d0e9baf..928d77fe143 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -40,6 +40,7 @@ MODULE_OBJS = \
metaengine.o \
minigames/dotgame.o \
minigames/sophie.o \
+ minigames/stalkingzazu.o \
profile.o
# This module can be built as a plugin
diff --git a/engines/mediastation/statemachine.h b/engines/mediastation/statemachine.h
new file mode 100644
index 00000000000..30bdcc5a9bc
--- /dev/null
+++ b/engines/mediastation/statemachine.h
@@ -0,0 +1,84 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MEDIASTATION_STATEMACHINE_H
+#define MEDIASTATION_STATEMACHINE_H
+
+#include "common/textconsole.h"
+#include "common/queue.h"
+
+namespace MediaStation {
+
+template<typename EventType>
+struct StateMachineEvent {
+ EventType eventType;
+};
+
+// The original had a very generalized finite state machine supported by state matrix, function pointer,
+// and other classes, but that was judged needlessly complex to reimplement here. The state transition logic
+// is embedded directly into this class.
+template<typename StateType, typename EventType>
+class StateMachine {
+public:
+ virtual ~StateMachine() {};
+
+ void queueEvent(EventType event);
+ void runIfNotNested();
+ void executeForever();
+
+protected:
+ StateType _currentState;
+ Common::Queue<EventType> _events;
+ bool _handlingEvents = false;
+ virtual void executeNextState(EventType eventType) = 0;
+ void warnOnInvalidTransition(EventType eventType);
+};
+
+template<typename StateType, typename EventType>
+void StateMachine<StateType, EventType>::queueEvent(EventType event) {
+ _events.push(event);
+}
+
+template<typename StateType, typename EventType>
+void StateMachine<StateType, EventType>::runIfNotNested() {
+ if (!_handlingEvents) {
+ _handlingEvents = true;
+ executeForever();
+ _handlingEvents = false;
+ }
+}
+
+template<typename StateType, typename EventType>
+void StateMachine<StateType, EventType>::executeForever() {
+ while (!_events.empty()) {
+ EventType eventType = _events.pop();
+ executeNextState(eventType);
+ }
+}
+
+template<typename StateType, typename EventType>
+void StateMachine<StateType, EventType>::warnOnInvalidTransition(EventType eventType) {
+ warning("Got invalid event %d for state %d", static_cast<uint>(eventType), static_cast<uint>(_currentState));
+}
+
+} // End of namespace MediaStation
+
+#endif
Commit: edb772820851c8ba92d6ddf007256f131725a3d3
https://github.com/scummvm/scummvm/commit/edb772820851c8ba92d6ddf007256f131725a3d3
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T20:59:49-04:00
Commit Message:
MEDIASTATION: Implement hardcoded Dalmatians maze minigame
Assisted-by: gpt-5.5 claude-sonnet-4.5
Changed paths:
A engines/mediastation/minigames/maze.cpp
A engines/mediastation/minigames/maze.h
engines/mediastation/actors/image.cpp
engines/mediastation/mediascript/collection.cpp
engines/mediastation/mediascript/function.cpp
engines/mediastation/mediascript/function.h
engines/mediastation/module.mk
diff --git a/engines/mediastation/actors/image.cpp b/engines/mediastation/actors/image.cpp
index 2aa6b991220..f0fecb0aac8 100644
--- a/engines/mediastation/actors/image.cpp
+++ b/engines/mediastation/actors/image.cpp
@@ -92,7 +92,7 @@ ScriptValue ImageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
}
void ImageActor::draw(DisplayContext &displayContext) {
- if (_isVisible) {
+ if (_asset != nullptr && _asset->bitmap != nullptr) {
Common::Point origin = getBbox().origin();
g_engine->getDisplayManager()->imageBlit(origin, _asset->bitmap, _dissolveFactor, &displayContext);
}
diff --git a/engines/mediastation/mediascript/collection.cpp b/engines/mediastation/mediascript/collection.cpp
index ebf964f8fac..13b4e5e0dd7 100644
--- a/engines/mediastation/mediascript/collection.cpp
+++ b/engines/mediastation/mediascript/collection.cpp
@@ -80,11 +80,13 @@ ScriptValue Collection::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
case kGetAtMethod: {
ARGCOUNTCHECK(1);
- uint index = static_cast<uint>(args[0].asFloat());
- if (index < size()) {
+ // Index can be -1, so make sure we check for that.
+ int index = static_cast<uint>(args[0].asFloat());
+ if (index >= 0 && (uint)index < size()) {
returnValue = operator[](index);
} else {
warning("%s: Index %d out of bounds %d", __func__, index, size());
+ returnValue.setToFloat(0.0);
}
break;
}
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index fac73f8f6c3..d9ef66a71c5 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -25,6 +25,7 @@
#include "mediastation/mediascript/function.h"
#include "mediastation/debugchannels.h"
#include "mediastation/mediastation.h"
+#include "mediastation/minigames/maze.h"
namespace MediaStation {
@@ -97,6 +98,8 @@ FunctionManager::~FunctionManager() {
delete it->_value;
}
_functions.clear();
+
+ delete _maze;
}
bool FunctionManager::attemptToReadFromStream(Chunk &chunk, uint sectionType) {
@@ -616,24 +619,20 @@ void FunctionManager::script_DebugPrint(Common::Array<ScriptValue> &args, Script
debug("%s", output.c_str());
}
-void FunctionManager::script_MazeGenerate(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
- warning("STUB: %s", __func__);
-}
-
-void FunctionManager::script_MazeApplyMoveMask(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
- warning("STUB: %s", __func__);
-}
-
-void FunctionManager::script_MazeSolve(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
- warning("STUB: %s", __func__);
-}
-
void FunctionManager::script_BeginTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
- warning("STUB: %s", __func__);
+ _timedIntervalStartInMs = g_engine->getTotalPlayTime();
}
void FunctionManager::script_EndTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
- warning("STUB: %s", __func__);
+ uint32 now = g_engine->getTotalPlayTime();
+ if (now < _timedIntervalStartInMs) {
+ warning("%s: Timed interval ended before it started", __func__);
+ return;
+ }
+
+ const uint32 millisecondsElapsed = now - _timedIntervalStartInMs;
+ const double secondsElapsed = millisecondsElapsed / 1000.0;
+ returnValue.setToFloat(secondsElapsed);
}
void FunctionManager::script_Checkers(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
diff --git a/engines/mediastation/mediascript/function.h b/engines/mediastation/mediascript/function.h
index fc7432870e8..6a6f2ff1e22 100644
--- a/engines/mediastation/mediascript/function.h
+++ b/engines/mediastation/mediascript/function.h
@@ -54,6 +54,10 @@ private:
uint32 _bytecodeSize = 0;
};
+namespace MazeMinigame {
+ class Maze;
+} // End of namespace MazeMinigame
+
class FunctionManager : public ParameterClient {
friend class Debugger;
@@ -66,10 +70,13 @@ public:
ScriptFunction *getFunctionById(uint functionId);
void deleteFunctionsForContext(uint contextId);
+ MazeMinigame::Maze *_maze = nullptr;
+
uint _scriptBlockCallDepth = 0;
private:
Common::HashMap<uint, ScriptFunction *> _functions;
+ uint32 _timedIntervalStartInMs = 0.0;
void script_GetPlatform(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
void script_Random(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
diff --git a/engines/mediastation/minigames/maze.cpp b/engines/mediastation/minigames/maze.cpp
new file mode 100644
index 00000000000..48bb52d79c1
--- /dev/null
+++ b/engines/mediastation/minigames/maze.cpp
@@ -0,0 +1,279 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/algorithm.h"
+#include "common/queue.h"
+
+#include "mediastation/mediascript/function.h"
+#include "mediastation/mediastation.h"
+#include "mediastation/minigames/maze.h"
+
+namespace MediaStation {
+
+// Interestingly, the titles that have these maze functions don't seem to have
+// scripts that actually call them. Thus, for now only the maze functionality that
+// is known to be actually used is reimplemented, namely the overloaded script_MazeSolve
+// method and the methods it calls.
+void FunctionManager::script_MazeGenerate(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+ // Doesn't seem to be actually called by scripts.
+ warning("STUB: %s", __func__);
+}
+
+void FunctionManager::script_MazeApplyMoveMask(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+ // Doesn't seem to be actually called by scripts.
+ warning("STUB: %s", __func__);
+}
+
+void FunctionManager::script_MazeSolve(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+ if (args.size() == 0) {
+ // newMaze_clear
+ delete _maze;
+ _maze = nullptr;
+
+ } else if (args.size() == 2) {
+ // newMaze_init
+ delete _maze;
+ _maze = new MazeMinigame::Maze(args);
+
+ } else if (args.size() == 4) {
+ // newMaze_solve
+ if (_maze != nullptr) {
+ _maze->solve(args, returnValue);
+ } else {
+ error("%s: Maze has not been initialized", __func__);
+ }
+
+ } else if (args.size() == 5) {
+ // Doesn't seem to be actually called by scripts.
+ error("%s: This pathfinding code has not been implemented due to no known titles that use it", __func__);
+
+ } else {
+ error("%s: Got invalid number of args", __func__);
+ }
+}
+
+MazeMinigame::Maze::Maze(const Common::Array<ScriptValue> &args) {
+ Collection *gridDataCollection = args[0].asCollection();
+ // args[1] is the precomputed routing table, which we won't
+ // use since we are just doing BFS.
+
+ const int16 rowCount = static_cast<int16>(gridDataCollection->operator[](gridDataCollection->size() - 2).asFloat());
+ const int16 columnCount = static_cast<int16>(gridDataCollection->operator[](gridDataCollection->size() - 1).asFloat());
+ _grid = new CellGrid(rowCount, columnCount);
+
+ // Read the per-cell direction masks out of the padded script layout into
+ // plain grid order (column outer, row inner).
+ Common::Array<int16> directionMasks;
+ directionMasks.reserve(rowCount * columnCount);
+ for (int16 column = 0; column < columnCount; column++) {
+ for (int16 row = 0; row < rowCount; row++) {
+ const int32 dataIndex = _grid->paddedIndexForCoord(CellCoord(row, column));
+ directionMasks.push_back(static_cast<int16>(gridDataCollection->operator[](dataIndex).asFloat()));
+ }
+ }
+
+ _grid->setDirections(directionMasks);
+}
+
+MazeMinigame::Maze::~Maze() {
+ delete _grid;
+ _grid = nullptr;
+}
+
+MazeMinigame::CellGraphType MazeMinigame::Cell::getType() const {
+ CellGraphType result = kObstacleCell;
+ switch (_directionMask) {
+ case 0x00:
+ result = kObstacleCell;
+ break;
+
+ // These other cases are technically not needed because only the segment table generation
+ // code used them. However, they are left here for completeness and later implementation
+ // of the original logic if desired.
+ case CellDirectionMask::kWest: // 0x01
+ case CellDirectionMask::kSouth: // 0x02
+ case CellDirectionMask::kEast: // 0x04
+ case CellDirectionMask::kNorth: // 0x08
+ result = kDeadEndNode;
+ break;
+
+ case (CellDirectionMask::kWest | CellDirectionMask::kSouth | CellDirectionMask::kEast): // 0x07
+ case (CellDirectionMask::kWest | CellDirectionMask::kSouth | CellDirectionMask::kNorth): // 0x0B
+ case (CellDirectionMask::kWest | CellDirectionMask::kEast | CellDirectionMask::kNorth): // 0x0D
+ case (CellDirectionMask::kSouth | CellDirectionMask::kEast | CellDirectionMask::kNorth): // 0x0E
+ case (CellDirectionMask::kWest | CellDirectionMask::kSouth | CellDirectionMask::kEast | CellDirectionMask::kNorth): // 0x0F
+ result = kJunctionNode;
+ break;
+
+ case (CellDirectionMask::kWest | CellDirectionMask::kSouth): // 0x03
+ case (CellDirectionMask::kWest | CellDirectionMask::kEast): // 0x05
+ case (CellDirectionMask::kSouth | CellDirectionMask::kEast): // 0x06
+ case (CellDirectionMask::kWest | CellDirectionMask::kNorth): // 0x09
+ case (CellDirectionMask::kSouth | CellDirectionMask::kNorth): // 0x0A
+ case (CellDirectionMask::kEast | CellDirectionMask::kNorth): // 0x0C
+ result = kCorridorCell;
+ break;
+
+ default:
+ warning("%s: Unexpected direction mask 0x%x", __func__, _directionMask);
+ break;
+ }
+
+ return result;
+}
+
+void MazeMinigame::Maze::solve(const Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+ // Cruella and the puppy arrive as 1-based row/column pairs.
+ const CellCoord cruellaCoord(
+ static_cast<int16>(args[0].asFloat() - 1),
+ static_cast<int16>(args[1].asFloat() - 1));
+ const CellCoord puppyCoord(
+ static_cast<int16>(args[2].asFloat() - 1),
+ static_cast<int16>(args[3].asFloat() - 1));
+
+ Collection *pathCollection = new Collection;
+ bool cruellaHasCaughtPuppy = (cruellaCoord == puppyCoord);
+ if (!cruellaHasCaughtPuppy) {
+ const Common::Array<CellCoord> path = _grid->shortestPath(cruellaCoord, puppyCoord);
+ if (path.empty()) {
+ // The maze is always fully connected, so this should never happen.
+ error("%s: No path found", __func__);
+ }
+
+ for (CellCoord coord : path) {
+ // Scripts expect these padded indices.
+ const int32 paddedGridIndex = _grid->paddedIndexForCoord(coord);
+ ScriptValue paddedGridIndexValue;
+ paddedGridIndexValue.setToFloat(paddedGridIndex);
+ pathCollection->push_back(paddedGridIndexValue);
+ }
+ }
+
+ // The return value owns this collection that holds the serialized
+ // path to send back to scripts.
+ returnValue.setToCollection(pathCollection);
+}
+
+int32 MazeMinigame::CellGrid::paddedIndexForCoord(CellCoord coord) {
+ // Maze data received from or sent to scripts is stored
+ // with a one-cell border of padding on each side.
+ return (coord.column + 1) * (_rows + 1) + (coord.row + 1);
+}
+
+MazeMinigame::CellGrid::CellGrid(int16 rowCount, int16 columnCount) : _rows(rowCount), _columns(columnCount) {
+ _cells.resize(_rows * _columns);
+}
+
+void MazeMinigame::CellGrid::setDirections(const Common::Array<int16> &directionMasks) {
+ // The masks arrive in grid order, so each linear slot is one cell.
+ for (uint cellIndex = 0; cellIndex < _cells.size(); cellIndex++) {
+ const CellCoord coord = coordForIndex(static_cast<int16>(cellIndex));
+ const int16 directionMask = directionMasks[cellIndex];
+ Cell &cell = at(coord);
+ cell = Cell(directionMask);
+
+ // Record the reachable neighbor in each open direction.
+ if (directionMask & CellDirectionMask::kWest) { // 0x01
+ cell._validNeighbors.push_back(indexForCoord(CellCoord(coord.row, coord.column - 1)));
+ }
+ if (directionMask & CellDirectionMask::kSouth) { // 0x02
+ cell._validNeighbors.push_back(indexForCoord(CellCoord(coord.row + 1, coord.column)));
+ }
+ if (directionMask & CellDirectionMask::kEast) { // 0x04
+ cell._validNeighbors.push_back(indexForCoord(CellCoord(coord.row, coord.column + 1)));
+ }
+ if (directionMask & CellDirectionMask::kNorth) { // 0x08
+ cell._validNeighbors.push_back(indexForCoord(CellCoord(coord.row - 1, coord.column)));
+ }
+ }
+}
+
+int16 MazeMinigame::CellGrid::indexForCoord(CellCoord coord) const {
+ return coord.row + coord.column * _rows;
+}
+
+MazeMinigame::CellCoord MazeMinigame::CellGrid::coordForIndex(int16 index) const {
+ return CellCoord(index % _rows, index / _rows);
+}
+
+MazeMinigame::Cell &MazeMinigame::CellGrid::at(CellCoord coord) {
+ return _cells[indexForCoord(coord)];
+}
+
+Common::Array<MazeMinigame::CellCoord> MazeMinigame::CellGrid::shortestPath(CellCoord start, CellCoord end) const {
+ Common::Array<CellCoord> path;
+ const Common::Array<uint> pathIndices = buildShortestPath(indexForCoord(start), indexForCoord(end));
+ for (uint cellIndex : pathIndices) {
+ path.push_back(coordForIndex(cellIndex));
+ }
+ return path;
+}
+
+Common::Array<uint> MazeMinigame::CellGrid::buildShortestPath(uint startIndex, uint endIndex) const {
+ const int32 NOT_VISITED = -1;
+ Common::Array<int32> predecessor(_cells.size(), NOT_VISITED);
+ predecessor[startIndex] = startIndex;
+
+ Common::Queue<uint> cellsToVisit;
+ cellsToVisit.push(startIndex);
+
+ // Do a standard BFS.
+ bool endWasReached = (predecessor[endIndex] != NOT_VISITED);
+ bool moreCellsToVisit = (!cellsToVisit.empty() && !endWasReached);
+ while (moreCellsToVisit) {
+ const uint currentCellIndex = cellsToVisit.pop();
+ const Cell ¤tCell = _cells[currentCellIndex];
+
+ for (int16 neighborCellIndex : currentCell._validNeighbors) {
+ bool alreadyVisited = (predecessor[neighborCellIndex] != NOT_VISITED);
+ if (alreadyVisited) {
+ continue;
+ }
+
+ if (_cells[neighborCellIndex].getType() == kObstacleCell) {
+ continue;
+ }
+
+ // We have not seen this cell yet but it is valid
+ // to move there, so we need to check it out.
+ predecessor[neighborCellIndex] = currentCellIndex;
+ cellsToVisit.push(neighborCellIndex);
+ }
+
+ endWasReached = (predecessor[endIndex] != NOT_VISITED);
+ moreCellsToVisit = (!cellsToVisit.empty() && !endWasReached);
+ }
+
+ Common::Array<uint> path;
+ if (endWasReached) {
+ // Trace the path from the end to the start, then reverse
+ // it to get the ordering scripts expect.
+ for (uint cellIndex = endIndex; cellIndex != startIndex; cellIndex = predecessor[cellIndex]) {
+ path.push_back(cellIndex);
+ }
+ path.push_back(startIndex);
+ Common::reverse(path.begin(), path.end());
+ }
+
+ return path;
+};
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/minigames/maze.h b/engines/mediastation/minigames/maze.h
new file mode 100644
index 00000000000..722108ad44d
--- /dev/null
+++ b/engines/mediastation/minigames/maze.h
@@ -0,0 +1,113 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MEDIASTATION_MINIGAMES_MAZE_H
+#define MEDIASTATION_MINIGAMES_MAZE_H
+
+#include "mediastation/actor.h"
+
+namespace MediaStation {
+
+namespace MazeMinigame {
+
+enum CellDirectionMask {
+ kWest = 0x01,
+ kSouth = 0x02,
+ kEast = 0x04,
+ kNorth = 0x08
+};
+
+enum CellGraphType {
+ kObstacleCell = 0,
+ kDeadEndNode = 1,
+ kJunctionNode = 2,
+ kCorridorCell = 3,
+};
+
+// A row/column position within the maze grid.
+struct CellCoord {
+ int16 row = 0;
+ int16 column = 0;
+
+ CellCoord(int16 row_ = 0, int16 column_ = 0) : row(row_), column(column_) {}
+
+ bool operator==(const CellCoord &other) const {
+ return row == other.row && column == other.column;
+ }
+};
+
+class Cell { // MazeElem
+public:
+ Cell(int16 directionMask = 0) : _directionMask(directionMask) {};
+
+ CellGraphType getType() const;
+ Common::Array<int16> _validNeighbors;
+
+private:
+ int16 _directionMask = 0;
+};
+
+// The original compressed the maze space down to "segments" (corridors connecting
+// junction/dead-end nodes) and used a pre-computed routing table for each level.
+// This seems way overkill for a 10x16 maze, so this reimplementation just does BFS.
+//
+// Another improvement from the original is that the linear cell indexing is purely
+// an implementation detail here. Callers address cells by coordinate and receive paths
+// back as coordinates.
+class CellGrid {
+public:
+ CellGrid(int16 rowCount, int16 columnCount);
+ void setDirections(const Common::Array<int16> &directionMasks);
+
+ // The shortest path from start to end inclusive, as coordinates.
+ // Empty if the two cells are not connected.
+ Common::Array<CellCoord> shortestPath(CellCoord start, CellCoord end) const;
+ int32 paddedIndexForCoord(CellCoord coord);
+
+private:
+ int16 indexForCoord(CellCoord coord) const;
+ CellCoord coordForIndex(int16 index) const;
+ Cell &at(CellCoord coord);
+ Common::Array<uint> buildShortestPath(uint startIndex, uint endIndex) const;
+
+ int16 _rows = 0;
+ int16 _columns = 0;
+ Common::Array<Cell> _cells;
+};
+
+// A simple maze with one player (puppy) and one enemy (Cruella). This translates the
+// padded cell indexing that scripts use to and from actual grid coordinates.
+class Maze {
+public:
+ Maze(const Common::Array<ScriptValue> &args); // newMaze_init
+ ~Maze();
+
+ void solve(const Common::Array<ScriptValue> &args, ScriptValue &returnValue); // newMaze_solve
+
+private:
+ CellGrid *_grid = nullptr;
+};
+
+} // End of namespace MazeMinigame
+
+} // End of namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 928d77fe143..503cbb0de3d 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -41,6 +41,7 @@ MODULE_OBJS = \
minigames/dotgame.o \
minigames/sophie.o \
minigames/stalkingzazu.o \
+ minigames/maze.o \
profile.o
# This module can be built as a plugin
Commit: dc7ddeb4bdcd7e54ad6c2778e08012fdfdf76aa2
https://github.com/scummvm/scummvm/commit/dc7ddeb4bdcd7e54ad6c2778e08012fdfdf76aa2
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-18T20:59:49-04:00
Commit Message:
MEDIASTATION: Implement hardcoded checkers minigame for Hercules
Assisted-by: gpt-5.5 claude-sonnet-4.5
Changed paths:
A engines/mediastation/minigames/checkers.cpp
A engines/mediastation/minigames/checkers.h
engines/mediastation/mediascript/codechunk.h
engines/mediastation/mediascript/function.cpp
engines/mediastation/mediascript/function.h
engines/mediastation/module.mk
diff --git a/engines/mediastation/mediascript/codechunk.h b/engines/mediastation/mediascript/codechunk.h
index 5b52b148ab5..74faa7c02cc 100644
--- a/engines/mediastation/mediascript/codechunk.h
+++ b/engines/mediastation/mediascript/codechunk.h
@@ -41,7 +41,7 @@ public:
private:
// This is not the number of recursive calls, it is as far is the script call stack is
// ever allowed to get.
- static const uint MAX_CALL_DEPTH = 0x0f;
+ static const uint MAX_CALL_DEPTH = 0x20;
void skipNextBlock();
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index d9ef66a71c5..3c7ebd154a5 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -26,6 +26,7 @@
#include "mediastation/debugchannels.h"
#include "mediastation/mediastation.h"
#include "mediastation/minigames/maze.h"
+#include "mediastation/minigames/checkers.h"
namespace MediaStation {
@@ -100,6 +101,7 @@ FunctionManager::~FunctionManager() {
_functions.clear();
delete _maze;
+ delete _checkers;
}
bool FunctionManager::attemptToReadFromStream(Chunk &chunk, uint sectionType) {
@@ -253,6 +255,7 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
break;
case kCheckersFunction:
+ FUNCARGMIN(1);
script_Checkers(args, returnValue);
break;
@@ -635,10 +638,6 @@ void FunctionManager::script_EndTimedInterval(Common::Array<ScriptValue> &args,
returnValue.setToFloat(secondsElapsed);
}
-void FunctionManager::script_Checkers(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
- warning("STUB: %s", __func__);
-}
-
void FunctionManager::script_Drawing(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: %s", __func__);
}
diff --git a/engines/mediastation/mediascript/function.h b/engines/mediastation/mediascript/function.h
index 6a6f2ff1e22..bb2568df2ec 100644
--- a/engines/mediastation/mediascript/function.h
+++ b/engines/mediastation/mediascript/function.h
@@ -58,6 +58,10 @@ namespace MazeMinigame {
class Maze;
} // End of namespace MazeMinigame
+namespace CheckersMinigame {
+ class Checkers;
+} // End of namespace CheckersMinigame
+
class FunctionManager : public ParameterClient {
friend class Debugger;
@@ -71,6 +75,7 @@ public:
void deleteFunctionsForContext(uint contextId);
MazeMinigame::Maze *_maze = nullptr;
+ CheckersMinigame::Checkers *_checkers = nullptr;
uint _scriptBlockCallDepth = 0;
diff --git a/engines/mediastation/minigames/checkers.cpp b/engines/mediastation/minigames/checkers.cpp
new file mode 100644
index 00000000000..8fb6a4fcfdf
--- /dev/null
+++ b/engines/mediastation/minigames/checkers.cpp
@@ -0,0 +1,979 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/algorithm.h"
+#include "common/system.h"
+
+#include "mediastation/debugchannels.h"
+#include "mediastation/mediascript/function.h"
+#include "mediastation/mediastation.h"
+#include "mediastation/minigames/checkers.h"
+
+namespace MediaStation {
+
+// For exact argument count.
+#define SUBCOMMANDARGCHECK(n) \
+ if (args.size() != (n)) { \
+ warning("%s: subcommand %d: expected %d argument%s, got %d", __func__, subcommand, (n), ((n) == 1 ? "" : "s"), args.size()); \
+ }
+
+void FunctionManager::script_Checkers(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+ const int subcommand = static_cast<int>(args[0].asFloat());
+ switch (subcommand) {
+ case CheckersMinigame::kCheckersCommandPiecesThatCanMove: {
+ // This indeed create a temporary board and then throws it away.
+ SUBCOMMANDARGCHECK(3);
+ Collection *collection = args[1].asCollection();
+ CheckersMinigame::Checkers checkers(collection);
+ if (!checkers.validateBoard()) {
+ warning("%s: Board failed validation, trying to continue", __func__);
+ }
+
+ // Not sure why the script couldn't have just provided the right side value
+ // to begin with, but we have to massage it here.
+ CheckersMinigame::Side side = (args[2].asFloat() == 0.0) ? CheckersMinigame::kBlackSide : CheckersMinigame::kWhiteSide;
+
+ Collection *result = new Collection();
+ Common::Array<CheckersMinigame::Pair> piecesThatCanMove = checkers.piecesThatCanMove(side);
+ for (const CheckersMinigame::Pair &pair : piecesThatCanMove) {
+ int linearCellIndex = pair.toLinearCellIndex();
+ ScriptValue value;
+ value.setToFloat(linearCellIndex);
+ result->push_back(value);
+ }
+
+ // The return value takes ownership of the collection.
+ returnValue.setToCollection(result);
+ break;
+ }
+
+ case CheckersMinigame::kCheckersGetValidMoves: {
+ // This indeed creates a temporary checkers board and then throws it away.
+ SUBCOMMANDARGCHECK(4);
+ Collection *collection = args[1].asCollection();
+ CheckersMinigame::Checkers checkers(collection);
+ if (!checkers.validateBoard()) {
+ warning("%s: Board failed validation, trying to continue", __func__);
+ }
+
+ int linearCellIndex = static_cast<int>(args[2].asFloat());
+ if (linearCellIndex < 0 || linearCellIndex > CheckersMinigame::TOTAL_PLAYABLE_CELLS - 1) {
+ error("%s: Linear cell index %d out of bounds", __func__, linearCellIndex);
+ }
+ bool getJumpsOnly = args[3].asBool();
+
+ Collection *result = new Collection();
+ CheckersMinigame::Pair position(linearCellIndex);
+ Common::Array<CheckersMinigame::Move> moves;
+ checkers.getValidMovesForPiece(position, moves, getJumpsOnly);
+ for (const CheckersMinigame::Move &move : moves) {
+ ScriptValue value;
+ Collection *moveCollection = move.asCollection();
+ value.setToCollection(moveCollection);
+ result->push_back(value);
+ }
+
+ // The return value takes ownership of the collection.
+ returnValue.setToCollection(result);
+ break;
+ }
+
+ case CheckersMinigame::kCheckersNewGame: {
+ SUBCOMMANDARGCHECK(4);
+ bool ok = false;
+ Collection *collection = args[1].asCollection();
+ delete g_engine->getFunctionManager()->_checkers;
+ g_engine->getFunctionManager()->_checkers = new CheckersMinigame::Checkers(collection);
+ CheckersMinigame::Checkers *checkers = g_engine->getFunctionManager()->_checkers;
+ if (!checkers->validateBoard()) {
+ warning("%s: Board failed validation, trying to continue", __func__);
+ }
+
+ // Not sure why the script couldn't have just provided the right side value
+ // to begin with, but we have to massage it here.
+ CheckersMinigame::Side side = (args[2].asFloat() == 0.0) ? CheckersMinigame::kBlackSide : CheckersMinigame::kWhiteSide;
+
+ // When playing against Hades, the chosen level seems to dictate Hades' move
+ // search depth. Known versions use the following values:
+ // - Level 1: 1 move ahead
+ // - Level 2: 2 moves ahead
+ // - Level 3: 4 moves ahead
+ int moveSearchDepth = MAX(1, static_cast<int>(args[3].asFloat()));
+ if (checkers->calculateAndSortAllMovesForSide(side)) {
+ checkers->prepareForBestMoveSearch(moveSearchDepth);
+ ok = true;
+ }
+
+ returnValue.setToBool(ok);
+ break;
+ }
+
+ case CheckersMinigame::kCheckersFindBestMove: {
+ SUBCOMMANDARGCHECK(2);
+ CheckersMinigame::Checkers *checkers = g_engine->getFunctionManager()->_checkers;
+ if (checkers == nullptr) {
+ error("%s: Board is not initialized", __func__);
+ }
+
+ bool foundBestMove = false;
+ double maxSearchTimeInSeconds = args[1].asTime();
+ int maxSearchTimeInMs = MAX(100, static_cast<int>(maxSearchTimeInSeconds * 1000.0));
+ foundBestMove = checkers->searchForBestMove(maxSearchTimeInMs);
+ returnValue.setToBool(foundBestMove);
+ break;
+ }
+
+ case CheckersMinigame::kCheckersTakeMove: {
+ CheckersMinigame::Checkers *checkers = g_engine->getFunctionManager()->_checkers;
+ if (checkers == nullptr) {
+ error("%s: Board is not initialized", __func__);
+ }
+
+ CheckersMinigame::Move &bestMove = checkers->getCalculatedBestMove();
+ Collection *result = bestMove.asCollection();
+
+ // The return value takes ownership of the collection.
+ returnValue.setToCollection(result);
+ break;
+ }
+
+ case CheckersMinigame::kCheckersCommandDebugPrint: {
+ // This indeed creates a temporary checkers board and then throws it away.
+ SUBCOMMANDARGCHECK(2);
+ CheckersMinigame::Checkers checkers(args[1].asCollection());
+ checkers.printToDebug();
+ break;
+ }
+
+ case CheckersMinigame::kCheckersCommandValidateBoard: {
+ // This looks like debug machinery left in the release version, but
+ // scripts actually do call it, so it is reimplemented here.
+ // This indeed creates a temporary checkers board and then throws it away.
+ SUBCOMMANDARGCHECK(2);
+ CheckersMinigame::Checkers checkers(args[1].asCollection());
+ returnValue.setToBool(checkers.validateBoard());
+ break;
+ }
+
+ default:
+ warning("%s: Got unimplemented checkers subcommand %d", __func__, subcommand);
+ break;
+ }
+}
+
+namespace CheckersMinigame {
+
+constexpr SideConstants BLACK_INFO = {
+ Side::kWhiteSide,
+ 0,
+ {
+ {-1, 1}, { 1, 1}, // Forward (toward row 7).
+ {-1, -1}, { 1, -1} // Backward.
+ }
+};
+
+constexpr SideConstants WHITE_INFO = {
+ Side::kBlackSide,
+ 7,
+ {
+ {-1, -1}, { 1, -1}, // Forward (toward row 0).
+ {-1, 1}, { 1, 1} // Backward.
+ }
+};
+
+// The two playable corner-adjacent squares on each side's home row. Keeping an
+// uncrowned man on one of these denies the opponent an easy promotion.
+static constexpr Pair BACK_RANK_DEFENDER_CELLS[] = {
+ {1, 0}, {5, 0}, // Black's home row.
+ {2, 7}, {6, 7} // White's home row.
+};
+
+const int kLossScore = -29999;
+const int kWinScore = 29999;
+
+Checkers::Checkers(Collection *collection) {
+ _board.resize(TOTAL_CELLS);
+
+ if (collection == nullptr) {
+ error("%s: Collection is null", __func__);
+ } else if (collection->size() < TOTAL_PLAYABLE_CELLS) {
+ error("%s: Source collection doesn't cover all playable cells", __func__);
+ }
+
+ Common::Array<int> cellValues(TOTAL_PLAYABLE_CELLS);
+ for (uint i = 0; i < TOTAL_PLAYABLE_CELLS; i++) {
+ cellValues[i] = static_cast<int>(collection->operator[](i).asFloat());
+ }
+ setBoard(cellValues);
+}
+
+void Checkers::setBoard(const Common::Array<int> &cellValues) {
+ // Parse the cell values from the script into an actual board.
+ uint padding = 1;
+ uint linearCellIndex = 0;
+ for (uint row = 0; row < ROW_COUNT; row++) {
+ for (uint col = 0; col < PLAYABLE_CELLS_PER_ROW; col++) {
+ Cell &cell = _board.cellAt(Pair(padding + col * 2, row));
+
+ int cellValueToParse = cellValues[linearCellIndex];
+ if (cellValueToParse == 0) {
+ cell.isKing = false;
+ cell.side = kUnoccupiedBySide;
+ cell.pieceId = 0;
+ } else {
+ if (cellValueToParse < 101) {
+ cell.isKing = false;
+ } else {
+ cell.isKing = true;
+ cellValueToParse -= 100;
+ }
+
+ cell.side = cellValueToParse < 13 ? kBlackSide : kWhiteSide;
+ cell.pieceId = cellValueToParse;
+ }
+
+ linearCellIndex++;
+ }
+
+ padding = (padding == 0);
+ }
+}
+
+Cell &Board::cellAt(const Pair &position) {
+ assert(position.isWithinBoard());
+ return operator[](position.x * COLUMN_COUNT + position.y);
+}
+
+const Cell &Board::cellAt(const Pair &position) const {
+ assert(position.isWithinBoard());
+ return operator[](position.x * COLUMN_COUNT + position.y);
+}
+
+Cell &Board::cellAt(uint linearIndex) {
+ return operator[](linearIndex);
+}
+
+void Checkers::updateMovesThatKing(const Board &board, Common::Array<Move> &moves) {
+ if (moves.empty()) {
+ return;
+ }
+
+ const Cell &firstMovePiece = board.cellAt(moves[0].from);
+ const int promotionRow = (firstMovePiece.side == kBlackSide) ? (ROW_COUNT - 1) : 0;
+
+ for (Move &move : moves) {
+ const Cell &movingPiece = board.cellAt(move.from);
+ if (!movingPiece.isKing && move.to.y == promotionRow) {
+ move.willBecomeKing = true;
+ }
+ }
+}
+
+bool Checkers::validateBoard() const {
+ bool valid = true;
+
+ int seen[25] = {};
+ seen[0] = -1;
+
+ Pair pos;
+ for (pos.y = 0; pos.y < ROW_COUNT; pos.y++) {
+ const int startX = (pos.y & 1) ? 0 : 1;
+ for (pos.x = startX; pos.x < COLUMN_COUNT; pos.x += 2) {
+ const Cell &cell = _board.cellAt(pos);
+ if (cell.pieceId == 0) {
+ continue;
+ }
+
+ if (cell.pieceId < 1 || cell.pieceId > 24) {
+ valid = false;
+ continue;
+ }
+
+ if (seen[cell.pieceId] != 0) {
+ valid = false;
+ } else {
+ seen[cell.pieceId] = pos.y * 8 + pos.x + 1;
+ }
+
+ if (cell.side == kBlackSide && pos.y == 7 && !cell.isKing) {
+ warning("%s: Piece %d should be kinged", __func__, cell.pieceId);
+ valid = false;
+ }
+
+ if (cell.side == kWhiteSide && pos.y == 0 && !cell.isKing) {
+ warning("%s: Piece %d should be kinged", __func__, cell.pieceId);
+ valid = false;
+ }
+ }
+ }
+
+ return valid;
+}
+
+void Checkers::printToDebug() const {
+ bool filledCellStartsAtEvenX = false;
+
+ for (int y = 0; y < ROW_COUNT; y++) {
+ Common::String border;
+ for (int x = 0; x < COLUMN_COUNT; x++) {
+ border += "+------";
+ }
+ border += "+";
+ debugC(5, kDebugMinigame, "%s", border.c_str());
+
+ Common::String row;
+ for (int x = 0; x < COLUMN_COUNT; x++) {
+ const Cell &cell = _board.cellAt(Pair(x, y));
+ Common::String cellText;
+
+ if (cell.side == kUnoccupiedBySide) {
+ cellText = (filledCellStartsAtEvenX == ((x % 2) == 0)) ? "####" : " ";
+ } else {
+ const char kingChar = cell.isKing ? 'k' : ' ';
+ const char sideChar = (cell.side == kBlackSide) ? 'B' : 'W';
+ cellText = Common::String::format("%c%2d%c", sideChar, ((cell.pieceId - 1) % 12) + 1, kingChar);
+ }
+
+ row += Common::String::format("| %s ", cellText.c_str());
+ }
+ row += "|";
+ debugC(5, kDebugMinigame, "%s", row.c_str());
+
+ filledCellStartsAtEvenX = !filledCellStartsAtEvenX;
+ }
+
+ Common::String border;
+ for (int x = 0; x < COLUMN_COUNT; x++) {
+ border += "+------";
+ }
+ border += "+\n";
+ debugC(5, kDebugMinigame, "%s", border.c_str());
+}
+
+Common::Array<Pair> Checkers::piecesThatCanMove(Side side) {
+ // Regenerate the legal moves list for this side.
+ Common::Array<Pair> moveablePieces;
+ Common::Array<Move> moves;
+ bool canDoAtLeastOneJump = checkForJumps(_board, side, nullptr, moves);
+ if (!canDoAtLeastOneJump) {
+ checkForSlides(_board, side, nullptr, moves);
+ }
+
+ bool movable[COLUMN_COUNT][ROW_COUNT] = {};
+ for (const Move &move : moves) {
+ movable[move.from.x][move.from.y] = true;
+ }
+
+ for (int y = 0; y < ROW_COUNT; y++) {
+ for (int x = 0; x < COLUMN_COUNT; x++) {
+ if (movable[x][y]) {
+ moveablePieces.push_back(Pair(x, y));
+ }
+ }
+ }
+
+ return moveablePieces;
+}
+
+void Checkers::getValidMovesForPiece(
+ const Pair &position, Common::Array<Move> &moves, bool getJumpsOnly) {
+ moves.clear();
+
+ const Cell &cell = _board.cellAt(position);
+ if (cell.isOccupied()) {
+ const bool canDoAtLeastOneJump = checkForJumps(_board, cell.side, &position, moves);
+ if (!getJumpsOnly) {
+ if (!canDoAtLeastOneJump) {
+ checkForSlides(_board, cell.side, &position, moves);
+ }
+ }
+
+ updateMovesThatKing(_board, moves);
+ }
+}
+
+bool Checkers::checkForJumps(const Board &board, Side mySide, const Pair *checkPosition, Common::Array<Move> &moves) {
+ const SideConstants &constants = constantsForSide(mySide);
+
+ // See if the current side can do any jumps (captures).
+ bool anyValidJumps = false;
+ Pair min(0, 0);
+ Pair max(COLUMN_COUNT - 1, ROW_COUNT - 1);
+ if (checkPosition != nullptr) {
+ min.x = max.x = checkPosition->x;
+ min.y = max.y = checkPosition->y;
+ }
+
+ for (int y = min.y; y <= max.y; y++) {
+ for (int x = min.x; x <= max.x; x++) {
+ // Only look at our pieces.
+ Pair from(x, y);
+ const Cell &piece = board.cellAt(from);
+ if (piece.side != mySide) {
+ continue;
+ }
+
+ // Kings can move diagonally backward and forward (4 total directions),
+ // but men can only move diagonally forward (2 total directions).
+ int totalValidDirections = piece.isKing ? 4 : 2;
+ for (int i = 0; i < totalValidDirections; i++) {
+ Pair capture = from + constants.validDirections[i];
+ Pair landing = capture + constants.validDirections[i];
+
+ // Make sure we will still land inside the board.
+ if (landing.isWithinBoard()) {
+ // We can jump iff the adjacent cell is occupied by an opponent...
+ if (board.cellAt(capture).side != constants.opponent) {
+ continue;
+ }
+
+ // ...and the cell that we'll land on is empty.
+ if (board.cellAt(landing).isOccupied()) {
+ continue;
+ }
+
+ addMove(moves, from, landing, &capture, false);
+ anyValidJumps = true;
+ }
+ }
+ }
+ }
+
+ return anyValidJumps;
+}
+
+bool Checkers::checkForSlides(const Board &board, Side side, const Pair *checkPosition, Common::Array<Move> &moves) {
+ const SideConstants &constants = constantsForSide(side);
+
+ // See if the current side can do any slides (moves without a capture).
+ bool anyValidSlides = false;
+ Pair min(0, 0);
+ Pair max(COLUMN_COUNT - 1, ROW_COUNT - 1);
+ if (checkPosition != nullptr) {
+ min.x = max.x = checkPosition->x;
+ min.y = max.y = checkPosition->y;
+ }
+
+ for (int y = min.y; y <= max.y; y++) {
+ for (int x = min.x; x <= max.x; x++) {
+ // Only look at our pieces.
+ Pair from(x, y);
+ const Cell &piece = board.cellAt(from);
+ if (piece.side != side) {
+ continue;
+ }
+
+ // Kings can move diagonally backward and forward (4 total directions),
+ // but men can only move diagonally forward (2 total directions).
+ int dirCount = piece.isKing ? 4 : 2;
+ for (int i = 0; i < dirCount; i++) {
+ Pair to = from + constants.validDirections[i];
+
+ if (to.isWithinBoard()) {
+ if (!board.cellAt(to).isOccupied()) {
+ addMove(moves, from, to, nullptr, false);
+ anyValidSlides = true;
+ }
+ }
+ }
+ }
+ }
+
+ return anyValidSlides;
+}
+
+void Checkers::addMove(Common::Array<Move> &moves, const Pair &from, const Pair &to, const Pair *captured, bool becameKing) {
+ Move move;
+ move.from = from;
+ move.to = to;
+
+ if (captured != nullptr) {
+ move.willCapture = true;
+ move.capture = *captured;
+ } else {
+ move.capture = {-1, -1};
+ }
+ moves.push_back(move);
+}
+
+void Checkers::makeMove(Board &board, const Move &move) {
+ Cell &fromCell = board.cellAt(move.from);
+ Cell &toCell = board.cellAt(move.to);
+ // The piece moves, and its original position is now unoccupied.
+ toCell = fromCell;
+ fromCell = Cell();
+
+ if (move.willBecomeKing) {
+ toCell.isKing = true;
+ }
+
+ if (move.willCapture) {
+ // The cell that we jumped over is now unoccupied,
+ // as the piece inside it was captured.
+ board.cellAt(move.capture) = Cell();
+ }
+
+ for (const Move &nextJump : move.nextJumps) {
+ makeMove(board, nextJump);
+ }
+}
+
+PieceCounts Checkers::getPieceCounts(const Board &board) {
+ PieceCounts counts;
+ Pair pos;
+ for (pos.y = 0; pos.y < ROW_COUNT; pos.y++) {
+ for (pos.x = 0; pos.x < COLUMN_COUNT; pos.x++) {
+ const Cell &cell = board.cellAt(pos);
+ if (cell.side == kBlackSide) {
+ cell.isKing ? counts.totalBlackKings++ : counts.totalBlackMen++;
+
+ } else if (cell.side == kWhiteSide) {
+ cell.isKing ? counts.totalWhiteKings++ : counts.totalWhiteMen++;
+ }
+ }
+ }
+ return counts;
+}
+
+int Checkers::getTotalBoardScoreForSide(const Board &board, Side ourSide) {
+ PieceCounts counts = getPieceCounts(board);
+ uint blackMaterialScore = (counts.totalBlackKings * 200) + (counts.totalBlackMen * 100);
+ uint whiteMaterialScore = (counts.totalWhiteKings * 200) + (counts.totalWhiteMen * 100);
+
+ Side opposingSide = kUnoccupiedBySide;
+ int homeRow = 0;
+ int score = 0;
+ switch (ourSide) {
+ case kBlackSide:
+ opposingSide = kWhiteSide;
+ score = blackMaterialScore - whiteMaterialScore;
+ break;
+
+ case kWhiteSide:
+ opposingSide = kBlackSide;
+ homeRow = 7;
+ score = whiteMaterialScore - blackMaterialScore;
+ break;
+
+ default:
+ error("%s: Got bad side %d", __func__, static_cast<uint>(ourSide));
+ }
+
+ if (!canMove(board, ourSide)) {
+ // We can't move, so we lost.
+ return kLossScore;
+
+ } else if (!canMove(board, opposingSide)) {
+ // Our opponent can't move, so we won.
+ return kWinScore;
+ }
+
+ // Award a small bonus for occupying one of the back-rank defender squares.
+ // Keeping an uncrowned piece here discourages the opponent from obtaining
+ // an easy promotion, so only men (not kings) get the bonus.
+ for (const Pair &defenderCell : BACK_RANK_DEFENDER_CELLS) {
+ const Cell &cell = board.cellAt(defenderCell);
+ if (cell.isMan()) {
+ score += (cell.side == ourSide) ? 7 : -7;
+ }
+ }
+
+ // Reward uncrowned pieces for advancing from their home row.
+ // This encourages men to make progress toward promotion.
+ Pair pos;
+ for (pos.y = 0; pos.y < ROW_COUNT; pos.y++) {
+ for (pos.x = 0; pos.x < COLUMN_COUNT; pos.x++) {
+ const Cell &cell = board.cellAt(pos);
+ if (cell.side == ourSide && !cell.isKing) {
+ score += ABS(homeRow - pos.y);
+ }
+ }
+ }
+
+ return score + getWeightedSquareScoreForSide(board, ourSide) - getWeightedSquareScoreForSide(board, opposingSide);
+}
+
+void Checkers::calculatePointsForMoves(const Board &board, Common::Array<Move> &moves) {
+ if (moves.empty()) {
+ return;
+ }
+
+ // The list of current moves should only contain moves from a single
+ // side at one time. Thus, we can determine this side by looking at the
+ // side of the first move.
+ const Side side = board.cellAt(moves[0].from).side;
+
+ for (Move &move : moves) {
+ Board updatedBoard = board;
+ makeMove(updatedBoard, move);
+ move.score = getTotalBoardScoreForSide(updatedBoard, side);
+ }
+}
+
+int Checkers::getWeightedSquareScoreForSide(const Board &board, Side side) {
+ PieceCounts counts = getPieceCounts(board);
+ const int ourKings = (side == kBlackSide) ? counts.totalBlackKings : counts.totalWhiteKings;
+ const int opponentKings = (side == kBlackSide) ? counts.totalWhiteKings : counts.totalBlackKings;
+ const bool behindInKings = ourKings < opponentKings;
+
+ int score = 0;
+ for (int linearCellIndex = 0; linearCellIndex < TOTAL_PLAYABLE_CELLS; linearCellIndex++) {
+ // Only look at our own cells.
+ const Cell &cell = board.cellAt(linearCellIndex);
+ if (cell.side != side) {
+ continue;
+ }
+
+ CellClass classification = CELL_CLASSIFICATION[linearCellIndex];
+ switch (classification) {
+ case kCellCorner:
+ // Corners are relatively safe because they can only be
+ // attacked from one direction.
+ if (cell.isKing && behindInKings) {
+ // If youâre behind in king count, keeping your kings
+ // in corners makes them harder to trap.
+ score += 10;
+ } else {
+ score += 2;
+ }
+ break;
+
+ case kCellNearCorner:
+ // These squares are adjacent to the corners and can become traps. A king
+ // here has fewer escape routes than one actually in the corner, so this
+ // is a much less ideal spot to be in than actually being in a corner.
+ if (cell.isKing && behindInKings) {
+ score -= 8;
+ }
+ break;
+
+ case kCellCenter:
+ // The four central playable squares can provide the greatest mobility and control.
+ score += 4;
+ break;
+
+ case kCellOuterCenter:
+ // Slightly discourage occupying these in favor of the true center.
+ score -= 1;
+ break;
+
+ case kCellNormal:
+ break;
+ }
+ }
+
+ return score;
+}
+
+bool Checkers::canMove(const Board &board, Side side) {
+ // Regenerate the legal moves list for this side.
+ Common::Array<Move> moves;
+ bool canMove = checkForJumps(board, side, nullptr, moves);
+ if (!canMove) {
+ canMove = checkForSlides(board, side, nullptr, moves);
+ }
+
+ return canMove;
+}
+
+int Checkers::alphaBetaEvaluate(
+ const Board &board, Move &parentMove, Side sideToSolve, Side sideToMove, int alpha, int beta, int currentSearchDepth, uint32 searchDeadlineInMs) {
+ // The original used an iterative search with an explicit state machine (and manually managed stack),
+ // but that was judged needlessly complex to reimplement here. For modern hardware, just do it recursively.
+
+ Common::Array<Move> moves;
+ bool searchTimeExpired = static_cast<int32>(g_system->getMillis() - searchDeadlineInMs) >= 0;
+ bool forceEndSearch = searchTimeExpired || currentSearchDepth > _maxSearchDepth;
+ if (forceEndSearch) {
+ // Just recommend the best move we found thus far.
+ return getTotalBoardScoreForSide(board, sideToSolve);
+ }
+
+ // If we just jumped but can jump again from our new position, we must do so.
+ // If we both captured and became a king on the same turn, we are already
+ // at the last row on the board, so it is not possible to jump again from this position.
+ bool canMaybeJumpAgain = parentMove.willCapture && !parentMove.willBecomeKing;
+ bool requireContinuationJump = false;
+ if (canMaybeJumpAgain) {
+ const Side continuingSide = board.cellAt(parentMove.to).side;
+ requireContinuationJump = checkForJumps(board, continuingSide, &parentMove.to, moves);
+ if (requireContinuationJump) {
+ // We must jump again, so ensure the turn remains ours.
+ sideToMove = continuingSide;
+ }
+ }
+
+ Side nextSideToMove = kUnoccupiedBySide;
+ int nextSearchDepth = currentSearchDepth;
+ if (requireContinuationJump) {
+ nextSideToMove = sideToMove;
+ updateMovesThatKing(board, moves);
+ calculatePointsForMoves(board, moves);
+ sortMovesByScore(moves);
+ // We do not treat continuation jumps as separate moves in terms of
+ // the search depth limit.
+ } else {
+ nextSideToMove = constantsForSide(sideToMove).opponent;
+ calculateAndSortMovesForSide(board, sideToMove, moves);
+ nextSearchDepth++;
+ }
+
+ bool searchIsFinished = moves.empty();
+ if (searchIsFinished) {
+ return getTotalBoardScoreForSide(board, sideToSolve);
+ }
+
+ int bestScore = 0;
+ if (sideToMove == sideToSolve) {
+ // It's our turn (the side we're solving for/maximizing player).
+ // Evaluate every legal move and keep the highest score found thus far.
+ // Thus, we start with the lowest possible score (a loss).
+ bestScore = kLossScore;
+ for (Move &move : moves) {
+ Board updatedBoard = board;
+ makeMove(updatedBoard, move);
+
+ const int score = alphaBetaEvaluate(updatedBoard, move, sideToSolve, nextSideToMove, alpha, beta, nextSearchDepth, searchDeadlineInMs);
+ move.score = score;
+ if (score > bestScore) {
+ // This is the best move we've seen so far, so record it.
+ bestScore = score;
+ if (requireContinuationJump) {
+ parentMove.nextJumps.clear();
+ parentMove.nextJumps.push_back(move);
+ }
+ }
+
+ // Raise the lower bound on the score this position can achieve.
+ alpha = MAX(alpha, bestScore);
+ if (alpha >= beta) {
+ // No remaining move can affect the final result.
+ // Stop searching this branch.
+ break;
+ }
+ }
+
+ } else {
+ // It's our opponent's turn (minimizing player). Evaluate every legal move and
+ // assume they choose the move that give us the lowest score.
+ bestScore = kWinScore;
+ for (Move &move : moves) {
+ Board updatedBoard = board;
+ makeMove(updatedBoard, move);
+ const int score = alphaBetaEvaluate(updatedBoard, move, sideToSolve, nextSideToMove, alpha, beta, nextSearchDepth, searchDeadlineInMs);
+ move.score = score;
+
+ if (score < bestScore) {
+ // Record the worst score (from our perspective) seen so far.
+ bestScore = score;
+ if (requireContinuationJump) {
+ parentMove.nextJumps.clear();
+ parentMove.nextJumps.push_back(move);
+ }
+ }
+
+ // Lower the upper bound on the score this position can achieve.
+ beta = MIN(beta, bestScore);
+ if (alpha >= beta) {
+ // No remaining move can affect the final result, so stop searching
+ // this branch early.
+ break;
+ }
+ }
+ }
+
+ sortMovesByScore(moves);
+ return bestScore;
+}
+
+void Checkers::prepareForBestMoveSearch(int moveSearchDepth) {
+ _maxSearchDepth = MAX(1, moveSearchDepth);
+
+ if (_moves.empty()) {
+ _sideToMove = kUnoccupiedBySide;
+ _bestMove = Move();
+ } else {
+ // The list of valid moves only contains moves from one side or the other
+ // at any given time. So we can determine the current side just by looking
+ // at the first move.
+ _sideToMove = _board.cellAt(_moves[0].from).side;
+ updateMovesThatKing(_board, _moves);
+ calculatePointsForMoves(_board, _moves);
+ sortMovesByScore(_moves);
+ _bestMove = _moves[0];
+ }
+}
+
+bool Checkers::searchForBestMove(int maxSearchTimeInMs) {
+ // The original had this timing logic to make sure finding the best move
+ // didn't take too much time. This should not be a concern on modern hardware,
+ // but we will include it for accuracy to the original.
+ maxSearchTimeInMs = MAX(1, maxSearchTimeInMs);
+ const uint32 searchDeadlineInMs = g_system->getMillis() + maxSearchTimeInMs;
+
+ if (_sideToMove == kUnoccupiedBySide) {
+ if (_moves.empty()) {
+ // TODO: Not sure why white is chosen here as black usually goes first?
+ _sideToMove = kWhiteSide;
+ } else {
+ _sideToMove = _board.cellAt(_moves[0].from).side;
+ }
+ }
+
+ bool canMove = calculateAndSortAllMovesForSide(_sideToMove) && !_moves.empty();
+ if (!canMove) {
+ return false;
+ }
+
+ // Search each legal move from the current position. As better moves are
+ // found, raise alpha so subsequent searches can prune more aggressively.
+ int alpha = kLossScore;
+ const int beta = kWinScore;
+ for (Move &move : _moves) {
+ Board speculativeBoard = _board;
+ makeMove(speculativeBoard, move);
+ move.score = alphaBetaEvaluate(
+ speculativeBoard, move, _sideToMove,
+ constantsForSide(_sideToMove).opponent,
+ alpha, beta, 0, searchDeadlineInMs);
+
+ // Track the best score from the current position so later searches can prune sooner.
+ alpha = MAX(alpha, move.score);
+ }
+
+ // Determine if there's more than one best move.
+ sortMovesByScore(_moves);
+ const int bestScore = _moves[0].score;
+ uint tiedMoves = 0;
+ for (const Move &move : _moves) {
+ if (move.score == bestScore) {
+ tiedMoves++;
+ } else {
+ break;
+ }
+ }
+ // If there is more than one best move, choose one randomly.
+ uint bestMoveIndex = 0;
+ if (tiedMoves > 1) {
+ bestMoveIndex = g_engine->_randomSource.getRandomNumber(tiedMoves - 1);
+ }
+ _bestMove = _moves[bestMoveIndex];
+
+ return true;
+}
+
+Move &Checkers::getCalculatedBestMove() {
+ return _bestMove;
+}
+
+Collection *Move::asCollection() const {
+ // Serialize the move back out to a collection so scripts can use it.
+ Collection *collection = new Collection();
+ ScriptValue value;
+ value.setToFloat(from.toLinearCellIndex());
+ collection->push_back(value);
+
+ const Move *currentMove = this;
+ while (currentMove != nullptr) {
+ value.setToFloat(currentMove->to.toLinearCellIndex());
+ collection->push_back(value);
+
+ int captured = -1;
+ if (currentMove->willCapture == true) {
+ captured = currentMove->capture.toLinearCellIndex();
+ }
+
+ if (currentMove->willBecomeKing) {
+ captured += 100;
+ }
+
+ value.setToFloat(captured);
+ collection->push_back(value);
+
+ currentMove = currentMove->nextJumps.empty() ? nullptr : ¤tMove->nextJumps[0];
+ }
+
+ // The caller takes ownership of the collection.
+ return collection;
+}
+
+bool Checkers::calculateAndSortMovesForSide(const Board &board, Side side, Common::Array<Move> &moves) {
+ // Regenerate the legal moves list for this side.
+ moves.clear();
+ bool canMove = true;
+ bool canDoAtLeastOneJump = checkForJumps(board, side, nullptr, moves);
+ if (!canDoAtLeastOneJump) {
+ canMove = checkForSlides(board, side, nullptr, moves);
+ }
+
+ if (canMove) {
+ updateMovesThatKing(board, moves);
+ calculatePointsForMoves(board, moves);
+ sortMovesByScore(moves);
+ }
+
+ return canMove;
+}
+
+bool Checkers::calculateAndSortAllMovesForSide(Side side) {
+ return calculateAndSortMovesForSide(_board, side, _moves);
+}
+
+void Checkers::sortMovesByScore(Common::Array<Move> &moves) {
+ // We can't easily use a Common::SortedArray here because the sort key (score)
+ // is mutated after the elements are in the array. So we will explicitly sort
+ // at the right times to ensure this invariant is respected.
+ Common::sort(moves.begin(), moves.end(), MoveScoreGreater());
+}
+
+const SideConstants &Checkers::constantsForSide(Side side) const {
+ switch (side) {
+ case kBlackSide:
+ return BLACK_INFO;
+
+ case kWhiteSide:
+ return WHITE_INFO;
+
+ default:
+ error("%s: Got bad side %d", __func__, static_cast<uint>(side));
+ }
+}
+
+Pair::Pair(int linearCellIndex) {
+ y = linearCellIndex / 4;
+ x = (linearCellIndex % 4) * 2 + ((y + 1) & 1);
+}
+
+int Pair::toLinearCellIndex() const {
+ return y * 4 + (x >> 1);
+}
+
+bool Pair::isWithinBoard() const {
+ return (x >= 0 && x < COLUMN_COUNT) && (y >= 0 && y < ROW_COUNT);
+}
+
+Pair Pair::operator+(const Pair &other) const {
+ return Pair(x + other.x, y + other.y);
+}
+
+} // End of namespace CheckersMinigame
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/minigames/checkers.h b/engines/mediastation/minigames/checkers.h
new file mode 100644
index 00000000000..5b812196aae
--- /dev/null
+++ b/engines/mediastation/minigames/checkers.h
@@ -0,0 +1,192 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MEDIASTATION_MINIGAMES_CHECKERS_H
+#define MEDIASTATION_MINIGAMES_CHECKERS_H
+
+#include "common/array.h"
+
+#include "mediastation/actor.h"
+#include "mediastation/mediascript/collection.h"
+
+namespace MediaStation {
+
+namespace CheckersMinigame {
+
+const int COLUMN_COUNT = 8;
+const int ROW_COUNT = 8;
+const int PLAYABLE_CELLS_PER_ROW = 4;
+const int TOTAL_CELLS = COLUMN_COUNT * ROW_COUNT;
+const int TOTAL_PLAYABLE_CELLS = ROW_COUNT * PLAYABLE_CELLS_PER_ROW;
+
+enum Side {
+ kUnoccupiedBySide = 0,
+ // "Black" is actually red in the game.
+ // Hades always plays black.
+ kBlackSide = 1,
+ kWhiteSide = 2
+};
+
+enum CellClass {
+ kCellNormal,
+ kCellCorner,
+ kCellNearCorner,
+ kCellOuterCenter,
+ kCellCenter
+};
+
+// Classification used by the positional evaluation score heuristic.
+// Indexed by the standard linear playable-cell numbering.
+static constexpr CellClass CELL_CLASSIFICATION[TOTAL_PLAYABLE_CELLS] = {
+ kCellCorner, kCellNearCorner, kCellNearCorner, kCellNormal,
+ kCellCorner, kCellNormal, kCellNormal, kCellNormal,
+ kCellNormal, kCellNormal, kCellNormal, kCellOuterCenter,
+ kCellOuterCenter, kCellCenter, kCellCenter, kCellNormal,
+ kCellNormal, kCellCenter, kCellCenter, kCellOuterCenter,
+ kCellOuterCenter, kCellNormal, kCellNormal, kCellNormal,
+ kCellNormal, kCellNormal, kCellNormal, kCellCorner,
+ kCellNormal, kCellNearCorner, kCellNearCorner, kCellCorner
+};
+
+struct Cell {
+ // Scripts use pieceId to track which piece image to show where.
+ int pieceId = 0;
+ Side side = kUnoccupiedBySide;
+ bool isKing = false;
+
+ bool isOccupied() const { return side != kUnoccupiedBySide; }
+ bool isMan() const { return isOccupied() && !isKing; }
+};
+
+// The coordinates of a particular cell on the checkers board.
+struct Pair {
+ int x = 0;
+ int y = 0;
+
+ constexpr Pair() = default;
+ constexpr Pair(int x_, int y_) : x(x_), y(y_) {}
+
+ Pair(int linearCellIndex); // notationToPair
+ int toLinearCellIndex() const; // pairToNotation
+
+ bool isWithinBoard() const;
+ Pair operator+(const Pair &other) const;
+};
+
+struct SideConstants {
+ Side opponent;
+ int homeRow;
+ CheckersMinigame::Pair validDirections[4];
+};
+
+struct PieceCounts {
+ int totalBlackMen = 0;
+ int totalBlackKings = 0;
+ int totalWhiteMen = 0;
+ int totalWhiteKings = 0;
+};
+
+struct Move {
+ Pair from;
+ Pair to;
+ Pair capture;
+ bool willCapture = false;
+ bool willBecomeKing = false;
+ int score = 0;
+ Common::Array<Move> nextJumps;
+
+ Collection *asCollection() const; // moveToCollection
+};
+
+enum CheckersCommand {
+ kCheckersCommandPiecesThatCanMove = 1,
+ kCheckersGetValidMoves = 2,
+ kCheckersNewGame = 3,
+ kCheckersFindBestMove = 4,
+ kCheckersTakeMove = 5,
+ kCheckersCommandDebugPrint = -2,
+ kCheckersCommandValidateBoard = -1
+};
+
+// The full 8x8 checkers grid. Although only the dark squares are ever playable,
+// every square is stored so that a Pair's (x, y) coordinates map directly onto
+// the backing storage.
+class Board : public Common::Array<Cell> {
+public:
+ Cell &cellAt(const Pair &position);
+ const Cell &cellAt(const Pair &position) const;
+ Cell &cellAt(uint linearIndex);
+};
+
+// A simple checkers engine that uses minimax with alpha/beta pruning.
+class Checkers {
+public:
+ Checkers(Collection *collection); // checkers_initBoard
+
+ void setBoard(const Common::Array<int> &squares);
+ Common::Array<CheckersMinigame::Pair> piecesThatCanMove(Side side);
+ void getValidMovesForPiece(
+ const Pair &position, Common::Array<Move> &moves, bool capturesOnly); // movesOfPiece
+ bool calculateAndSortAllMovesForSide(Side side);
+ void prepareForBestMoveSearch(int depth);
+ bool searchForBestMove(int maxSearchTimeInMs);
+ Move &getCalculatedBestMove(); // bestMoveCalculated
+ bool validateBoard() const;
+ void printToDebug() const;
+
+private:
+ Board _board;
+ Common::Array<Move> _moves;
+ int _maxSearchDepth = 1;
+ Side _sideToMove = kUnoccupiedBySide;
+ Move _bestMove;
+
+ const SideConstants &constantsForSide(Side side) const;
+ bool checkForJumps(const Board &board, Side side, const Pair *checkPosition, Common::Array<Move> &moves); // checkForJumps
+ bool checkForSlides(const Board &board, Side side, const Pair *checkPosition, Common::Array<Move> &moves);
+ void updateMovesThatKing(const Board &board, Common::Array<Move> &moves);
+ bool canMove(const Board &board, Side side);
+ void addMove(Common::Array<Move> &moves, const Pair &start, const Pair &end, const Pair *captured, bool becameKing);
+ void makeMove(Board &board, const Move &move);
+
+ bool calculateAndSortMovesForSide(const Board &board, Side side, Common::Array<Move> &moves);
+ void calculatePointsForMoves(const Board &board, Common::Array<Move> &moves); // calculateMovePoints
+ int alphaBetaEvaluate(const Board &board, Move &parentMove, Side maximizingSide, Side sideToMove, int alpha, int beta, int ply, uint32 deadline);
+
+ // This is the score that the minimax is optimizing.
+ int getTotalBoardScoreForSide(const Board &board, Side side);
+ int getWeightedSquareScoreForSide(const Board &board, Side side);
+ PieceCounts getPieceCounts(const Board &board);
+ void sortMovesByScore(Common::Array<Move> &moves);
+};
+
+// Functor to allow sorting moves by their score.
+struct MoveScoreGreater {
+ bool operator()(const CheckersMinigame::Move &left, const CheckersMinigame::Move &right) const {
+ return left.score > right.score;
+ }
+};
+
+} // End of namespace CheckersMinigame
+
+} // End of namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 503cbb0de3d..4297c51c209 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -38,6 +38,7 @@ MODULE_OBJS = \
mediascript/scriptvalue.o \
mediastation.o \
metaengine.o \
+ minigames/checkers.o \
minigames/dotgame.o \
minigames/sophie.o \
minigames/stalkingzazu.o \
More information about the Scummvm-git-logs
mailing list