[Scummvm-git-logs] scummvm master -> 0ecd32937639db72c4c0d3881b7a412c6220d869
dreammaster
noreply at scummvm.org
Sat Oct 10 07:48:04 UTC 2026
This automated email contains information about 14 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
1c5d9ca93e HOPKINS: Expose optional WBASE enhancements
7c5ab1b081 HOPKINS: WBASE: Add opaque navigation map
282cf1bfb2 HOPKINS: WBASE: Add deterministic autoplay routing
738128b64d HOPKINS: WBASE: Add the "A" autoplay chooser
01000a967e HOPKINS: WBASE: Handle enhancement state better
57cc316938 HOPKINS: Expose forced WBASE autoplay
81232090e5 HOPKINS: WBASE: Add forced autoplay mode
5c26611e1c HOPKINS: WBASE: Add pointer controls to WBASE enhancements
48d38249ef HOPKINS: Validate debugger teleport destinations
09eb971be5 HOPKINS: Fix STP scanner classification
3f70cd94f8 HOPKINS: WBASE: Keep cursor only in HUD and chooser
264d124d83 HOPKINS: WBASE: Preserve input until the next game tick
bc23974b18 HOPKINS: WBASE: Add compact gameplay controls to the HUD
0ecd329376 HOPKINS: WBASE: Add viewport exit-or-fire input
Commit: 1c5d9ca93e9f1293b3d3c08e6626563a367f4ce6
https://github.com/scummvm/scummvm/commit/1c5d9ca93e9f1293b3d3c08e6626563a367f4ce6
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: Expose optional WBASE enhancements
Add a default-off WBASE Enhancements option for Windows, and register
separate WBASE-only keymap. Reserve M action.
Assisted-by: Codex:GPT-5.4
Changed paths:
A engines/hopkins/base_enhancements.h
engines/hopkins/detection.h
engines/hopkins/detection_tables.h
engines/hopkins/hopkins.h
engines/hopkins/metaengine.cpp
diff --git a/engines/hopkins/base_enhancements.h b/engines/hopkins/base_enhancements.h
new file mode 100644
index 00000000000..ffe829912d5
--- /dev/null
+++ b/engines/hopkins/base_enhancements.h
@@ -0,0 +1,50 @@
+/* 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 HOPKINS_BASE_ENHANCEMENTS_H
+#define HOPKINS_BASE_ENHANCEMENTS_H
+
+#include "common/scummsys.h"
+#include "common/str.h"
+
+namespace Hopkins {
+
+class BaseData;
+class BaseEngine;
+
+static const char *const kWBASEEnhancementsConfigKey = "wbase_enhancements";
+static const char *const kWBASEEnhancementsKeymapId = "hopkins-wbase-enhancements";
+
+class WBASEEnhancements {
+public:
+ explicit WBASEEnhancements(const Common::String &targetName);
+
+ bool enabled() const { return _enabled; }
+ bool navigationMapEnabled() const { return _enabled; }
+ void renderNavigationMap(const BaseData &data, const BaseEngine &engine, byte *framebuffer) const;
+
+private:
+ bool _enabled;
+};
+
+} // End of namespace Hopkins
+
+#endif // HOPKINS_BASE_ENHANCEMENTS_H
diff --git a/engines/hopkins/detection.h b/engines/hopkins/detection.h
index 22ec70adea2..ae76a30bf92 100644
--- a/engines/hopkins/detection.h
+++ b/engines/hopkins/detection.h
@@ -32,6 +32,7 @@ struct HopkinsGameDescription {
#define GAMEOPTION_GORE_DEFAULT_ON GUIO_GAMEOPTIONS1
#define GAMEOPTION_GORE_DEFAULT_OFF GUIO_GAMEOPTIONS2
+#define GAMEOPTION_WBASE_ENHANCEMENTS GUIO_GAMEOPTIONS3
} // End of namespace Hopkins
diff --git a/engines/hopkins/detection_tables.h b/engines/hopkins/detection_tables.h
index 6999c63ed30..99100789e6a 100644
--- a/engines/hopkins/detection_tables.h
+++ b/engines/hopkins/detection_tables.h
@@ -68,7 +68,7 @@ static const HopkinsGameDescription gameDescriptions[] = {
Common::ES_ESP,
Common::kPlatformWindows,
ADGF_NO_FLAGS,
- GUIO2(GAMEOPTION_GORE_DEFAULT_ON, GUIO_NOMIDI)
+ GUIO3(GAMEOPTION_GORE_DEFAULT_ON, GAMEOPTION_WBASE_ENHANCEMENTS, GUIO_NOMIDI)
},
},
{
@@ -80,7 +80,7 @@ static const HopkinsGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformWindows,
ADGF_NO_FLAGS,
- GUIO2(GAMEOPTION_GORE_DEFAULT_OFF, GUIO_NOMIDI)
+ GUIO3(GAMEOPTION_GORE_DEFAULT_OFF, GAMEOPTION_WBASE_ENHANCEMENTS, GUIO_NOMIDI)
},
},
{
@@ -92,7 +92,7 @@ static const HopkinsGameDescription gameDescriptions[] = {
Common::RU_RUS,
Common::kPlatformWindows,
ADGF_NO_FLAGS,
- GUIO2(GAMEOPTION_GORE_DEFAULT_ON, GUIO_NOMIDI)
+ GUIO3(GAMEOPTION_GORE_DEFAULT_ON, GAMEOPTION_WBASE_ENHANCEMENTS, GUIO_NOMIDI)
},
},
{
@@ -128,7 +128,7 @@ static const HopkinsGameDescription gameDescriptions[] = {
Common::FR_FRA,
Common::kPlatformWindows,
ADGF_NO_FLAGS,
- GUIO2(GAMEOPTION_GORE_DEFAULT_ON, GUIO_NOMIDI)
+ GUIO3(GAMEOPTION_GORE_DEFAULT_ON, GAMEOPTION_WBASE_ENHANCEMENTS, GUIO_NOMIDI)
},
},
@@ -141,7 +141,7 @@ static const HopkinsGameDescription gameDescriptions[] = {
Common::PL_POL,
Common::kPlatformWindows,
ADGF_NO_FLAGS,
- GUIO2(GAMEOPTION_GORE_DEFAULT_OFF, GUIO_NOMIDI)
+ GUIO3(GAMEOPTION_GORE_DEFAULT_OFF, GAMEOPTION_WBASE_ENHANCEMENTS, GUIO_NOMIDI)
},
},
diff --git a/engines/hopkins/hopkins.h b/engines/hopkins/hopkins.h
index afeae2e7851..895c214437d 100644
--- a/engines/hopkins/hopkins.h
+++ b/engines/hopkins/hopkins.h
@@ -75,7 +75,8 @@ enum HOPKINSAction {
kActionBaseFire,
kActionBaseUse,
kActionBaseToggleTextures,
- kActionBaseMenu
+ kActionBaseMenu,
+ kActionWBASEEnhancementsNavigationMap
};
enum HopkinsDebugChannels {
diff --git a/engines/hopkins/metaengine.cpp b/engines/hopkins/metaengine.cpp
index 5a7fb8d66e3..6225bfc8a66 100644
--- a/engines/hopkins/metaengine.cpp
+++ b/engines/hopkins/metaengine.cpp
@@ -35,6 +35,7 @@
#include "backends/keymapper/standard-actions.h"
#include "hopkins/detection.h"
+#include "hopkins/base_enhancements.h"
#define MAX_SAVES 99
@@ -65,6 +66,18 @@ static const ADExtraGuiOptionsMap optionsList[] = {
}
},
+ {
+ GAMEOPTION_WBASE_ENHANCEMENTS,
+ {
+ _s("WBASE enhancements"),
+ _s("Enable navigation/usability enhancements for Windows base shooter"),
+ kWBASEEnhancementsConfigKey,
+ false,
+ 0,
+ 0
+ }
+ },
+
AD_EXTRA_GUI_OPTIONS_TERMINATOR
};
@@ -207,6 +220,8 @@ Common::KeymapArray HopkinsMetaEngine::initKeymaps(const char *target) const {
Keymap *gameKeyMap = new Keymap(Keymap::kKeymapTypeGame, "game-shortcuts", _("Game keymappings"));
Keymap *baseKeyMap = new Keymap(Keymap::kKeymapTypeGame, "hopkins-base", _("Underwater base shooter"));
baseKeyMap->setEnabled(false);
+ Keymap *wbaseEnhancementsKeyMap = new Keymap(Keymap::kKeymapTypeGame, kWBASEEnhancementsKeymapId, _("WBASE enhancements"));
+ wbaseEnhancementsKeyMap->setEnabled(false);
Action *act;
@@ -305,10 +320,16 @@ Common::KeymapArray HopkinsMetaEngine::initKeymaps(const char *target) const {
act->addDefaultInputMapping("JOY_BACK");
baseKeyMap->addAction(act);
- KeymapArray keymaps(3);
+ act = new Action("WBASE_ENHANCEMENTS_MAP", _("WBASE enhancements: open navigation map"));
+ act->setCustomEngineActionEvent(kActionWBASEEnhancementsNavigationMap);
+ act->addDefaultInputMapping("m");
+ wbaseEnhancementsKeyMap->addAction(act);
+
+ KeymapArray keymaps(4);
keymaps[0] = engineKeyMap;
keymaps[1] = gameKeyMap;
keymaps[2] = baseKeyMap;
+ keymaps[3] = wbaseEnhancementsKeyMap;
return keymaps;
}
Commit: 7c5ab1b081927da74763fbafd14ae1de2a947d6f
https://github.com/scummvm/scummvm/commit/7c5ab1b081927da74763fbafd14ae1de2a947d6f
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: WBASE: Add opaque navigation map
Derive a paused navigation view from BASE.MAP data and
live state. Label walls, doors, transition exits, active guards,
and the player's position and facing direction.
Assisted-by: Codex:GPT-5.4
Changed paths:
A engines/hopkins/base_enhancements.cpp
engines/hopkins/base.cpp
engines/hopkins/base.h
engines/hopkins/module.mk
diff --git a/engines/hopkins/base.cpp b/engines/hopkins/base.cpp
index 1d98ad27140..7967d84c737 100644
--- a/engines/hopkins/base.cpp
+++ b/engines/hopkins/base.cpp
@@ -117,12 +117,12 @@ private:
};
BaseGame::BaseGame(HopkinsEngine *vm) :
- _vm(vm), _engine(nullptr), _renderer(nullptr), _entry(nullptr), _result(-1),
+ _vm(vm), _wbaseEnhancements(vm->getTargetName()), _engine(nullptr), _renderer(nullptr), _entry(nullptr), _result(-1),
_keymapsSwitched(false), _defaultKeymapWasEnabled(false),
- _shortcutKeymapWasEnabled(false), _baseKeymapWasEnabled(false),
+ _shortcutKeymapWasEnabled(false), _baseKeymapWasEnabled(false), _wbaseEnhancementsKeymapWasEnabled(false),
_inputSuspended(false), _mainMenuRequested(false),
_presentationRefreshRequested(false), _timingResetRequested(false),
- _quitRequested(false) {
+ _wbaseEnhancementsNavigationMapVisible(false), _quitRequested(false) {
_framebuffer.resize(kBaseFrameWidth * kBaseFrameHeight);
Common::fill(_audioLoaded, _audioLoaded + ARRAYSIZE(_audioLoaded), false);
}
@@ -192,6 +192,7 @@ BaseRunResult BaseGame::run(int entryId) {
_mainMenuRequested = false;
_presentationRefreshRequested = false;
_timingResetRequested = false;
+ _wbaseEnhancementsNavigationMapVisible = false;
_quitRequested = false;
_result = -1;
renderFrame();
@@ -211,7 +212,7 @@ BaseRunResult BaseGame::run(int entryId) {
}
if (_result != -1 || _quitRequested || _vm->shouldQuit())
break;
- if (_inputSuspended) {
+ if (_inputSuspended || _wbaseEnhancementsNavigationMapVisible) {
previousTime = now;
accumulator = 0;
_vm->_soundMan->checkSounds();
@@ -240,7 +241,6 @@ BaseRunResult BaseGame::run(int entryId) {
if (_quitRequested || _vm->shouldQuit())
return BaseRunResult(kBaseRunQuit);
-
return BaseRunResult(kBaseRunCompleted, _result);
}
@@ -352,6 +352,7 @@ void BaseGame::switchKeymaps(bool entering) {
Common::Keymap *defaultKeymap = keymapper->getKeymap(kDefaultKeymapId);
Common::Keymap *shortcutKeymap = keymapper->getKeymap(kShortcutKeymapId);
Common::Keymap *baseKeymap = keymapper->getKeymap(kBaseKeymapId);
+ Common::Keymap *wbaseEnhancementsKeymap = keymapper->getKeymap(kWBASEEnhancementsKeymapId);
if (entering) {
if (_keymapsSwitched)
@@ -359,12 +360,15 @@ void BaseGame::switchKeymaps(bool entering) {
_defaultKeymapWasEnabled = defaultKeymap && defaultKeymap->isEnabled();
_shortcutKeymapWasEnabled = shortcutKeymap && shortcutKeymap->isEnabled();
_baseKeymapWasEnabled = baseKeymap && baseKeymap->isEnabled();
+ _wbaseEnhancementsKeymapWasEnabled = wbaseEnhancementsKeymap && wbaseEnhancementsKeymap->isEnabled();
if (defaultKeymap)
defaultKeymap->setEnabled(false);
if (shortcutKeymap)
shortcutKeymap->setEnabled(false);
if (baseKeymap)
baseKeymap->setEnabled(true);
+ if (wbaseEnhancementsKeymap)
+ wbaseEnhancementsKeymap->setEnabled(_wbaseEnhancements.enabled());
_keymapsSwitched = true;
} else if (_keymapsSwitched) {
if (defaultKeymap)
@@ -373,6 +377,8 @@ void BaseGame::switchKeymaps(bool entering) {
shortcutKeymap->setEnabled(_shortcutKeymapWasEnabled);
if (baseKeymap)
baseKeymap->setEnabled(_baseKeymapWasEnabled);
+ if (wbaseEnhancementsKeymap)
+ wbaseEnhancementsKeymap->setEnabled(_wbaseEnhancementsKeymapWasEnabled);
_keymapsSwitched = false;
}
}
@@ -450,8 +456,24 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
_input.toggleTextures = true;
break;
case kActionBaseMenu:
- if (pressed)
- _mainMenuRequested = true;
+ if (pressed) {
+ if (_wbaseEnhancementsNavigationMapVisible) {
+ _wbaseEnhancementsNavigationMapVisible = false;
+ _input = BaseInputState();
+ _timingResetRequested = true;
+ renderFrame();
+ } else {
+ _mainMenuRequested = true;
+ }
+ }
+ break;
+ case kActionWBASEEnhancementsNavigationMap:
+ if (pressed && _wbaseEnhancements.navigationMapEnabled()) {
+ _wbaseEnhancementsNavigationMapVisible = !_wbaseEnhancementsNavigationMapVisible;
+ _input = BaseInputState();
+ _timingResetRequested = true;
+ renderFrame();
+ }
break;
default:
break;
@@ -497,8 +519,12 @@ void BaseGame::processSoundEvents() {
void BaseGame::renderFrame() {
if (!_renderer || !_engine || _framebuffer.empty())
return;
- _renderer->render(*_engine, _framebuffer.begin());
- Common::copy(_framebuffer.begin(), _framebuffer.end(), _vm->_graphicsMan->_frontBuffer); _vm->_graphicsMan->addDirtyRect(0, 0, kBaseFrameWidth, kBaseFrameHeight);
+ if (_wbaseEnhancementsNavigationMapVisible)
+ _wbaseEnhancements.renderNavigationMap(_data, *_engine, _framebuffer.begin());
+ else
+ _renderer->render(*_engine, _framebuffer.begin());
+ Common::copy(_framebuffer.begin(), _framebuffer.end(), _vm->_graphicsMan->_frontBuffer);
+ _vm->_graphicsMan->addDirtyRect(0, 0, kBaseFrameWidth, kBaseFrameHeight);
_vm->_graphicsMan->updateScreen();
}
diff --git a/engines/hopkins/base.h b/engines/hopkins/base.h
index 5eda1c2f906..a52cde6ef7c 100644
--- a/engines/hopkins/base.h
+++ b/engines/hopkins/base.h
@@ -26,6 +26,7 @@
#include "hopkins/base_data.h"
#include "hopkins/base_engine.h"
+#include "hopkins/base_enhancements.h"
#include "common/array.h"
#include "common/scummsys.h"
@@ -95,6 +96,7 @@ private:
void setEngineActive(bool active);
HopkinsEngine *_vm;
+ WBASEEnhancements _wbaseEnhancements;
BaseData _data;
BaseEngine *_engine;
BaseRenderer *_renderer;
@@ -107,10 +109,12 @@ private:
bool _defaultKeymapWasEnabled;
bool _shortcutKeymapWasEnabled;
bool _baseKeymapWasEnabled;
+ bool _wbaseEnhancementsKeymapWasEnabled;
bool _inputSuspended;
bool _mainMenuRequested;
bool _presentationRefreshRequested;
bool _timingResetRequested;
+ bool _wbaseEnhancementsNavigationMapVisible;
bool _quitRequested;
};
diff --git a/engines/hopkins/base_enhancements.cpp b/engines/hopkins/base_enhancements.cpp
new file mode 100644
index 00000000000..613519466a5
--- /dev/null
+++ b/engines/hopkins/base_enhancements.cpp
@@ -0,0 +1,144 @@
+/* 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 "hopkins/base_enhancements.h"
+
+#include "hopkins/base_data.h"
+#include "hopkins/base_engine.h"
+#include "hopkins/base_types.h"
+
+#include "common/config-manager.h"
+#include "common/util.h"
+
+namespace Hopkins {
+
+namespace {
+
+static byte nearestPaletteColor(const byte *palette, int red, int green, int blue) {
+ int bestDistance = 0x7fffffff;
+ byte bestColor = 0;
+ for (int color = 0; color < 256; ++color) {
+ const int offset = color * 3;
+ const int redDelta = palette[offset] - red;
+ const int greenDelta = palette[offset + 1] - green;
+ const int blueDelta = palette[offset + 2] - blue;
+ const int distance = redDelta * redDelta + greenDelta * greenDelta + blueDelta * blueDelta;
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestColor = color;
+ }
+ }
+ return bestColor;
+}
+
+static void drawPixel(byte *framebuffer, int x, int y, byte color) {
+ if (x >= 0 && x < kBaseFrameWidth && y >= 0 && y < kBaseFrameHeight)
+ framebuffer[y * kBaseFrameWidth + x] = color;
+}
+
+static void drawLine(byte *framebuffer, int x0, int y0, int x1, int y1, byte color) {
+ const int deltaX = ABS(x1 - x0);
+ const int stepX = x0 < x1 ? 1 : -1;
+ const int deltaY = -ABS(y1 - y0);
+ const int stepY = y0 < y1 ? 1 : -1;
+ int error = deltaX + deltaY;
+ for (;;) {
+ drawPixel(framebuffer, x0, y0, color);
+ if (x0 == x1 && y0 == y1)
+ break;
+ const int doubledError = error * 2;
+ if (doubledError >= deltaY) {
+ error += deltaY;
+ x0 += stepX;
+ }
+ if (doubledError <= deltaX) {
+ error += deltaX;
+ y0 += stepY;
+ }
+ }
+}
+
+} // End of anonymous namespace
+
+WBASEEnhancements::WBASEEnhancements(const Common::String &targetName) :
+ _enabled(ConfMan.getBool(kWBASEEnhancementsConfigKey, targetName)) {
+}
+
+void WBASEEnhancements::renderNavigationMap(const BaseData &data, const BaseEngine &engine, byte *framebuffer) const {
+ if (!framebuffer)
+ return;
+
+ const byte *palette = data.palette();
+ const byte backgroundColor = nearestPaletteColor(palette, 0, 0, 0);
+ const byte wallColor = nearestPaletteColor(palette, 190, 190, 190);
+ const byte doorColor = nearestPaletteColor(palette, 255, 210, 0);
+ const byte exitColor = nearestPaletteColor(palette, 0, 220, 255);
+ const byte guardColor = nearestPaletteColor(palette, 255, 70, 20);
+ const byte playerColor = nearestPaletteColor(palette, 40, 255, 60);
+ Common::fill(framebuffer, framebuffer + kBaseFrameWidth * kBaseFrameHeight, backgroundColor);
+
+ static const int kMapScale = 2;
+ static const int kMapPixelWidth = kBaseMapWidth * kMapScale;
+ static const int kMapPixelHeight = kBaseMapHeight * kMapScale;
+ const int mapLeft = (kBaseFrameWidth - kMapPixelWidth) / 2;
+ const int mapTop = (kBaseFrameHeight - kMapPixelHeight) / 2;
+
+ for (int mapY = 0; mapY < kBaseMapHeight; ++mapY) {
+ for (int mapX = 0; mapX < kBaseMapWidth; ++mapX) {
+ const uint16 code = data.mapCodeXY(mapX, mapY);
+ const byte lowCode = code & 0xff;
+ if (!code || lowCode >= 0xfc)
+ continue;
+
+ byte color = wallColor;
+ if (lowCode == kBaseDoorXCode || lowCode == kBaseDoorYCode)
+ color = doorColor;
+ else if (lowCode == kBaseExitBitmap)
+ color = exitColor;
+
+ const int pixelX = mapLeft + mapX * kMapScale;
+ const int pixelY = mapTop + mapY * kMapScale;
+ for (int y = 0; y < kMapScale; ++y)
+ for (int x = 0; x < kMapScale; ++x)
+ drawPixel(framebuffer, pixelX + x, pixelY + y, color);
+ }
+ }
+
+ for (int objectId = 1; objectId <= kBaseMaxObjects; ++objectId) {
+ const BaseObject &object = engine.object(objectId);
+ if (!object.active || object.mode == kBaseObjectDead)
+ continue;
+ const int pixelX = mapLeft + (object.x >> kBaseCellShift) * kMapScale;
+ const int pixelY = mapTop + (object.y >> kBaseCellShift) * kMapScale;
+ drawPixel(framebuffer, pixelX, pixelY, guardColor);
+ }
+
+ const int playerX = mapLeft + (engine.playerX() >> kBaseCellShift) * kMapScale;
+ const int playerY = mapTop + (engine.playerY() >> kBaseCellShift) * kMapScale;
+ for (int y = -2; y <= 2; ++y)
+ for (int x = -2; x <= 2; ++x)
+ drawPixel(framebuffer, playerX + x, playerY + y, playerColor);
+ const int directionX = playerX + (int)(((int64)data.cosQ16(engine.playerAngle()) * 10) >> kBaseFixedShift);
+ const int directionY = playerY + (int)(((int64)data.sinQ16(engine.playerAngle()) * 10) >> kBaseFixedShift);
+ drawLine(framebuffer, playerX, playerY, directionX, directionY, playerColor);
+}
+
+} // End of namespace Hopkins
diff --git a/engines/hopkins/module.mk b/engines/hopkins/module.mk
index dd3eff7082a..faa28fb4fbc 100644
--- a/engines/hopkins/module.mk
+++ b/engines/hopkins/module.mk
@@ -6,6 +6,7 @@ MODULE_OBJS := \
base_data.o \
base_engine.o \
base_renderer.o \
+ base_enhancements.o \
computer.o \
debugger.o \
dialogs.o \
Commit: 282cf1bfb2cf01dbfd012f1f41e9b6d494ffe806
https://github.com/scummvm/scummvm/commit/282cf1bfb2cf01dbfd012f1f41e9b6d494ffe806
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: WBASE: Add deterministic autoplay routing
Add a Hopkins-local controller that derives routes from the live ACK
X and Y edge grids and the six data-defined transition cells. Generate
only the same turn, movement, fire, and use input available to a
player.
Assisted-by: Codex:GPT-5.4
Changed paths:
A engines/hopkins/base_enhancements_autoplay.cpp
A engines/hopkins/base_enhancements_autoplay.h
engines/hopkins/module.mk
diff --git a/engines/hopkins/base_enhancements_autoplay.cpp b/engines/hopkins/base_enhancements_autoplay.cpp
new file mode 100644
index 00000000000..6059a281d94
--- /dev/null
+++ b/engines/hopkins/base_enhancements_autoplay.cpp
@@ -0,0 +1,402 @@
+/* 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 "hopkins/base_enhancements_autoplay.h"
+
+#include "hopkins/base_data.h"
+#include "hopkins/base_engine.h"
+#include "hopkins/base_types.h"
+
+#include "common/algorithm.h"
+#include "common/translation.h"
+#include "common/util.h"
+#include "graphics/font.h"
+#include "graphics/fontman.h"
+#include "graphics/pixelformat.h"
+#include "graphics/surface.h"
+
+namespace Hopkins {
+
+namespace {
+
+struct AutoplayDestination {
+ int roomId;
+ int exitMapPos;
+ int approachMapPos;
+ const char *label;
+};
+
+// WBASE transition cells and the adjacent cells used to approach them.
+static const AutoplayDestination kAutoplayDestinations[] = {
+ { 94, 0x0860, 0x0861, "Exit 1: Sector 4" },
+ { 95, 0x087a, 0x0879, "Exit 2: Sector 2" },
+ { 96, 0x01df, 0x021f, "Exit 3: Office" },
+ { 97, 0x05ab, 0x05eb, "Exit 4: Laboratory" },
+ { 98, 0x0bab, 0x0b6b, "Exit 5: Resurrection" },
+ { 99, 0x01fb, 0x023b, "Exit 6: Bernie's room" }
+};
+
+static const int kAutoplayDestinationCount = ARRAYSIZE(kAutoplayDestinations);
+static const int kAutoplayAimTolerance = 24;
+static const int kAutoplayMoveTolerance = 48;
+static const int kAutoplayWaypointTolerance = 12;
+static const int kAutoplayCombatDistance = 600;
+static const int kAutoplayStuckTicks = 24;
+
+static int absoluteValue(int value) {
+ return value < 0 ? -value : value;
+}
+
+static int signedAngleDelta(int from, int to) {
+ int delta = normalizeBaseAngle(to - from);
+ if (delta > kBaseHalfTurn)
+ delta -= kBaseAngleCount;
+ return delta;
+}
+
+static bool traversableEdge(uint16 code) {
+ if (code == 0 || (code & kBaseWallPass))
+ return true;
+ const byte lowCode = code & 0xff;
+ return (lowCode == kBaseDoorXCode || lowCode == kBaseDoorYCode) && !(code & kBaseDoorLocked);
+}
+
+static bool canStep(const BaseEngine &engine, int from, int to) {
+ const int fromX = from & 63;
+ const int fromY = from >> 6;
+ const int toX = to & 63;
+ const int toY = to >> 6;
+ if (toX < 0 || toX >= kBaseMapWidth || toY < 0 || toY >= kBaseMapHeight)
+ return false;
+
+ if (toX == fromX + 1 && toY == fromY)
+ return traversableEdge(engine.xGrid()[from + 1]);
+ if (toX == fromX - 1 && toY == fromY)
+ return traversableEdge(engine.xGrid()[from]);
+ if (toX == fromX && toY == fromY + 1)
+ return traversableEdge(engine.yGrid()[from + kBaseMapWidth]);
+ if (toX == fromX && toY == fromY - 1)
+ return traversableEdge(engine.yGrid()[from]);
+ return false;
+}
+
+static byte nearestPaletteColor(const byte *palette, int red, int green, int blue) {
+ int bestDistance = 0x7fffffff;
+ byte bestColor = 0;
+ for (int color = 0; color < 256; ++color) {
+ const int offset = color * 3;
+ const int redDelta = palette[offset] - red;
+ const int greenDelta = palette[offset + 1] - green;
+ const int blueDelta = palette[offset + 2] - blue;
+ const int distance = redDelta * redDelta + greenDelta * greenDelta + blueDelta * blueDelta;
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestColor = color;
+ }
+ }
+ return bestColor;
+}
+
+static void fillRect(byte *framebuffer, int left, int top, int right, int bottom, byte color) {
+ left = MAX(left, 0);
+ top = MAX(top, 0);
+ right = MIN(right, kBaseFrameWidth - 1);
+ bottom = MIN(bottom, kBaseFrameHeight - 1);
+ for (int y = top; y <= bottom; ++y)
+ Common::fill(framebuffer + y * kBaseFrameWidth + left,
+ framebuffer + y * kBaseFrameWidth + right + 1, color);
+}
+
+static void drawFrame(byte *framebuffer, int left, int top, int right, int bottom, byte color) {
+ fillRect(framebuffer, left, top, right, top, color);
+ fillRect(framebuffer, left, bottom, right, bottom, color);
+ fillRect(framebuffer, left, top, left, bottom, color);
+ fillRect(framebuffer, right, top, right, bottom, color);
+}
+
+} // End of anonymous namespace
+
+WBASEEnhancementsAutoplay::WBASEEnhancementsAutoplay() {
+ reset();
+}
+
+void WBASEEnhancementsAutoplay::reset() {
+ _active = false;
+ _menuVisible = false;
+ _selectedDestination = 0;
+ _destination = -1;
+ _route.clear();
+ _routeIndex = 0;
+ _lastPlayerX = -1;
+ _lastPlayerY = -1;
+ _stuckTicks = 0;
+ _routeInvalidated = false;
+}
+
+void WBASEEnhancementsAutoplay::openMenu() {
+ _menuVisible = true;
+}
+
+void WBASEEnhancementsAutoplay::closeMenu() {
+ _menuVisible = false;
+}
+
+void WBASEEnhancementsAutoplay::selectPrevious() {
+ _selectedDestination = (_selectedDestination + kAutoplayDestinationCount - 1) % kAutoplayDestinationCount;
+}
+
+void WBASEEnhancementsAutoplay::selectNext() {
+ _selectedDestination = (_selectedDestination + 1) % kAutoplayDestinationCount;
+}
+
+void WBASEEnhancementsAutoplay::startSelected(const BaseEngine &engine) {
+ _destination = _selectedDestination;
+ _active = true;
+ _menuVisible = false;
+ _lastPlayerX = engine.playerX();
+ _lastPlayerY = engine.playerY();
+ _stuckTicks = 0;
+ _routeInvalidated = false;
+ rebuildRoute(engine);
+}
+
+void WBASEEnhancementsAutoplay::cancel() {
+ _active = false;
+ _menuVisible = false;
+ _destination = -1;
+ _route.clear();
+ _routeIndex = 0;
+}
+
+bool WBASEEnhancementsAutoplay::rebuildRoute(const BaseEngine &engine) {
+ _route.clear();
+ _routeIndex = 0;
+ _routeInvalidated = false;
+ if (_destination < 0 || _destination >= kAutoplayDestinationCount)
+ return false;
+
+ const int start = baseWorldMapIndex(engine.playerX(), engine.playerY());
+ const int goal = kAutoplayDestinations[_destination].approachMapPos;
+ if (start < 0 || start >= kBaseMapCellCount || goal < 0 || goal >= kBaseMapCellCount)
+ return false;
+
+ int parent[kBaseMapCellCount];
+ int queue[kBaseMapCellCount];
+ Common::fill(parent, parent + ARRAYSIZE(parent), -2);
+ int queueBegin = 0;
+ int queueEnd = 0;
+ queue[queueEnd++] = start;
+ parent[start] = -1;
+
+ static const int kOffsets[] = { 1, -1, kBaseMapWidth, -kBaseMapWidth };
+ while (queueBegin < queueEnd && parent[goal] == -2) {
+ const int current = queue[queueBegin++];
+ for (uint direction = 0; direction < ARRAYSIZE(kOffsets); ++direction) {
+ const int next = current + kOffsets[direction];
+ if (next < 0 || next >= kBaseMapCellCount || parent[next] != -2)
+ continue;
+ if (!canStep(engine, current, next))
+ continue;
+ parent[next] = current;
+ queue[queueEnd++] = next;
+ }
+ }
+
+ if (parent[goal] == -2)
+ return false;
+
+ Common::Array<int> reverseRoute;
+ for (int mapPos = goal; mapPos >= 0; mapPos = parent[mapPos])
+ reverseRoute.push_back(mapPos);
+ for (int index = (int)reverseRoute.size() - 1; index >= 0; --index)
+ _route.push_back(reverseRoute[index]);
+ _routeIndex = _route.size() > 1 ? 1 : 0;
+ return true;
+}
+
+int WBASEEnhancementsAutoplay::findVisibleGuard(const BaseEngine &engine) const {
+ int bestGuard = 0;
+ int bestDistance = kAutoplayCombatDistance + 1;
+ for (int id = 1; id <= kBaseMaxObjects; ++id) {
+ const BaseObject &object = engine.object(id);
+ if (!object.active || object.passable || object.mode > kBaseObjectFlee)
+ continue;
+ const int distance = engine.objectDistance(object);
+ if (distance > kAutoplayCombatDistance || distance >= bestDistance)
+ continue;
+
+ const int angle = engine.objectAngle(object.x - engine.playerX(), object.y - engine.playerY());
+ const BaseRayHit wall = engine.castRay(angle, kBaseViewHalfWidth);
+ if (wall.hit && wall.distance + 24 < distance)
+ continue;
+ bestGuard = id;
+ bestDistance = distance;
+ }
+ return bestGuard;
+}
+
+bool WBASEEnhancementsAutoplay::aimAt(const BaseEngine &engine, int targetX, int targetY,
+ BaseInputState &input) const {
+ const int desiredAngle = engine.objectAngle(targetX - engine.playerX(), targetY - engine.playerY());
+ const int delta = signedAngleDelta(engine.playerAngle(), desiredAngle);
+ if (delta > kAutoplayAimTolerance)
+ input.turnRight = true;
+ else if (delta < -kAutoplayAimTolerance)
+ input.turnLeft = true;
+ return absoluteValue(delta) <= kAutoplayAimTolerance;
+}
+
+void WBASEEnhancementsAutoplay::updateStuckState(const BaseEngine &engine, bool triedToMove) {
+ if (!triedToMove || engine.playerX() != _lastPlayerX || engine.playerY() != _lastPlayerY) {
+ _stuckTicks = 0;
+ } else if (++_stuckTicks >= kAutoplayStuckTicks) {
+ _routeInvalidated = true;
+ _stuckTicks = 0;
+ }
+ _lastPlayerX = engine.playerX();
+ _lastPlayerY = engine.playerY();
+}
+
+void WBASEEnhancementsAutoplay::update(const BaseEngine &engine, BaseInputState &input) {
+ input = BaseInputState();
+ if (!_active || _menuVisible || _destination < 0 || _destination >= kAutoplayDestinationCount)
+ return;
+
+ const int guardId = findVisibleGuard(engine);
+ if (guardId) {
+ const BaseObject &guard = engine.object(guardId);
+ if (aimAt(engine, guard.x, guard.y, input) && engine.weaponCounter() == 0)
+ input.fire = true;
+ updateStuckState(engine, false);
+ return;
+ }
+
+ const AutoplayDestination &destination = kAutoplayDestinations[_destination];
+ const int playerMapPos = baseWorldMapIndex(engine.playerX(), engine.playerY());
+ if (playerMapPos == destination.approachMapPos) {
+ const int exitX = (destination.exitMapPos & 63) * kBaseCellSize + kBaseCellSize / 2;
+ const int exitY = (destination.exitMapPos >> 6) * kBaseCellSize + kBaseCellSize / 2;
+ const int approachX = (destination.approachMapPos & 63) * kBaseCellSize + kBaseCellSize / 2;
+ const int approachY = (destination.approachMapPos >> 6) * kBaseCellSize + kBaseCellSize / 2;
+ const int centerDistance = absoluteValue(engine.playerX() - approachX) + absoluteValue(engine.playerY() - approachY);
+ if (centerDistance > kAutoplayWaypointTolerance) {
+ const bool aligned = aimAt(engine, approachX, approachY, input);
+ input.forward = aligned;
+ updateStuckState(engine, input.forward);
+ return;
+ }
+ if (aimAt(engine, exitX, exitY, input))
+ input.exitRequested = true;
+ updateStuckState(engine, false);
+ return;
+ }
+
+ if (_routeInvalidated || _route.empty() || _routeIndex >= _route.size())
+ rebuildRoute(engine);
+ if (_route.empty()) {
+ cancel();
+ return;
+ }
+
+ while (_routeIndex < _route.size() && playerMapPos == _route[_routeIndex]) {
+ const int waypointX = (_route[_routeIndex] & 63) * kBaseCellSize + kBaseCellSize / 2;
+ const int waypointY = (_route[_routeIndex] >> 6) * kBaseCellSize + kBaseCellSize / 2;
+ if (absoluteValue(engine.playerX() - waypointX) + absoluteValue(engine.playerY() - waypointY) >
+ kAutoplayWaypointTolerance)
+ break;
+ ++_routeIndex;
+ }
+
+ if (_routeIndex >= _route.size()) {
+ _routeInvalidated = true;
+ updateStuckState(engine, false);
+ return;
+ }
+
+ const int expectedCell = _routeIndex > 0 ? _route[_routeIndex - 1] : _route[0];
+ if (playerMapPos != expectedCell && playerMapPos != _route[_routeIndex]) {
+ _routeInvalidated = true;
+ updateStuckState(engine, false);
+ return;
+ }
+
+ const int waypointX = (_route[_routeIndex] & 63) * kBaseCellSize + kBaseCellSize / 2;
+ const int waypointY = (_route[_routeIndex] >> 6) * kBaseCellSize + kBaseCellSize / 2;
+ const int desiredAngle = engine.objectAngle(waypointX - engine.playerX(), waypointY - engine.playerY());
+ const int delta = signedAngleDelta(engine.playerAngle(), desiredAngle);
+ if (delta > kAutoplayAimTolerance)
+ input.turnRight = true;
+ else if (delta < -kAutoplayAimTolerance)
+ input.turnLeft = true;
+ input.forward = absoluteValue(delta) <= kAutoplayMoveTolerance;
+ updateStuckState(engine, input.forward);
+}
+
+void WBASEEnhancementsAutoplay::render(const BaseData &data, byte *framebuffer, bool showStatus) const {
+ if (!framebuffer || (!_menuVisible && (!_active || !showStatus)))
+ return;
+
+ Graphics::Surface surface;
+ surface.init(kBaseFrameWidth, kBaseFrameHeight, kBaseFrameWidth, framebuffer,
+ Graphics::PixelFormat::createFormatCLUT8());
+ const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
+ if (!font)
+ return;
+
+ const byte *palette = data.palette();
+ const byte background = nearestPaletteColor(palette, 0, 0, 0);
+ const byte border = nearestPaletteColor(palette, 190, 190, 190);
+ const byte text = nearestPaletteColor(palette, 255, 255, 255);
+ const byte selected = nearestPaletteColor(palette, 40, 255, 60);
+
+ if (_menuVisible) {
+ static const int kLeft = 42;
+ static const int kTop = 27;
+ static const int kRight = 277;
+ static const int kBottom = 172;
+ fillRect(framebuffer, kLeft, kTop, kRight, kBottom, background);
+ drawFrame(framebuffer, kLeft, kTop, kRight, kBottom, border);
+ font->drawString(&surface, _("WBASE AUTOPLAY"), kLeft + 4, kTop + 9,
+ kRight - kLeft - 7, text, Graphics::kTextAlignCenter);
+ font->drawString(&surface, _("Fight your way to:"), kLeft + 4, kTop + 27,
+ kRight - kLeft - 7, text, Graphics::kTextAlignCenter);
+ for (int index = 0; index < kAutoplayDestinationCount; ++index) {
+ const Common::U32String label = Common::U32String::format("%c %S",
+ index == _selectedDestination ? '>' : ' ', _(kAutoplayDestinations[index].label).c_str());
+ font->drawString(&surface, label, kLeft + 18, kTop + 45 + index * 12,
+ kRight - kLeft - 35, index == _selectedDestination ? selected : text);
+ }
+ font->drawString(&surface, _("Up/Down: choose Space: start Esc: cancel"),
+ kLeft + 4, kBottom - 15, kRight - kLeft - 7, text, Graphics::kTextAlignCenter);
+ return;
+ }
+
+ const Common::U32String status = Common::U32String::format(_("AUTO: %S A: route"),
+ _(kAutoplayDestinations[_destination].label).c_str());
+ const int statusWidth = MIN(font->getStringWidth(status) + 10, kBaseFrameWidth - 8);
+ const int statusLeft = (kBaseFrameWidth - statusWidth) / 2;
+ fillRect(framebuffer, statusLeft, 2, statusLeft + statusWidth - 1, 14, background);
+ drawFrame(framebuffer, statusLeft, 2, statusLeft + statusWidth - 1, 14, border);
+ font->drawString(&surface, status, statusLeft + 4, 4, statusWidth - 8, selected,
+ Graphics::kTextAlignCenter);
+}
+
+} // End of namespace Hopkins
diff --git a/engines/hopkins/base_enhancements_autoplay.h b/engines/hopkins/base_enhancements_autoplay.h
new file mode 100644
index 00000000000..d9ecb9d56f4
--- /dev/null
+++ b/engines/hopkins/base_enhancements_autoplay.h
@@ -0,0 +1,74 @@
+/* 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 HOPKINS_BASE_ENHANCEMENTS_AUTOPLAY_H
+#define HOPKINS_BASE_ENHANCEMENTS_AUTOPLAY_H
+
+#include "common/array.h"
+#include "common/scummsys.h"
+
+namespace Hopkins {
+
+class BaseData;
+class BaseEngine;
+struct BaseInputState;
+
+/** Drive WBASE by producing ordinary player input from live edge-grid routes. */
+class WBASEEnhancementsAutoplay {
+public:
+ WBASEEnhancementsAutoplay();
+
+ void reset();
+ bool active() const { return _active; }
+ bool menuVisible() const { return _menuVisible; }
+
+ void openMenu();
+ void closeMenu();
+ void selectPrevious();
+ void selectNext();
+ void startSelected(const BaseEngine &engine);
+ void cancel();
+
+ /** Replace the next simulation tick's input with autopilot controls. */
+ void update(const BaseEngine &engine, BaseInputState &input);
+ void render(const BaseData &data, byte *framebuffer, bool showStatus) const;
+
+private:
+ bool rebuildRoute(const BaseEngine &engine);
+ int findVisibleGuard(const BaseEngine &engine) const;
+ bool aimAt(const BaseEngine &engine, int targetX, int targetY, BaseInputState &input) const;
+ void updateStuckState(const BaseEngine &engine, bool triedToMove);
+
+ bool _active;
+ bool _menuVisible;
+ int _selectedDestination;
+ int _destination;
+ Common::Array<int> _route;
+ uint _routeIndex;
+ int _lastPlayerX;
+ int _lastPlayerY;
+ int _stuckTicks;
+ bool _routeInvalidated;
+};
+
+} // End of namespace Hopkins
+
+#endif // HOPKINS_BASE_ENHANCEMENTS_AUTOPLAY_H
diff --git a/engines/hopkins/module.mk b/engines/hopkins/module.mk
index faa28fb4fbc..fba8cf988a3 100644
--- a/engines/hopkins/module.mk
+++ b/engines/hopkins/module.mk
@@ -7,6 +7,7 @@ MODULE_OBJS := \
base_engine.o \
base_renderer.o \
base_enhancements.o \
+ base_enhancements_autoplay.o \
computer.o \
debugger.o \
dialogs.o \
Commit: 738128b64d866b000298d090dcad65378236156f
https://github.com/scummvm/scummvm/commit/738128b64d866b000298d090dcad65378236156f
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: WBASE: Add the "A" autoplay chooser
Expose the optional controller through A and a single opaque
Hopkins-style destination panel over a frozen gameplay frame. Number
and name all six choices from their data-defined exit mappings.
Add bounded stall recovery, geometry-derived safe lanes around door
jambs and split-wall corners, active-object avoidance, and decreasing
guard-chase priority.
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/hopkins/base.cpp
engines/hopkins/base.h
engines/hopkins/base_enhancements.cpp
engines/hopkins/base_enhancements.h
engines/hopkins/base_enhancements_autoplay.cpp
engines/hopkins/base_enhancements_autoplay.h
engines/hopkins/hopkins.h
engines/hopkins/metaengine.cpp
diff --git a/engines/hopkins/base.cpp b/engines/hopkins/base.cpp
index 7967d84c737..5f710122454 100644
--- a/engines/hopkins/base.cpp
+++ b/engines/hopkins/base.cpp
@@ -25,6 +25,7 @@
#include "hopkins/base_renderer.h"
#include "hopkins/events.h"
+#include "hopkins/font.h"
#include "hopkins/globals.h"
#include "hopkins/graphics.h"
#include "hopkins/hopkins.h"
@@ -47,6 +48,14 @@ static const char *const kBaseKeymapId = "hopkins-base";
static const char *const kDefaultKeymapId = "hopkins-default";
static const char *const kShortcutKeymapId = "game-shortcuts";
+// WBASE renders a 320x200 frame through the original Hopkins 640x480
+// presentation: 2x pixels with 30-pixel top and bottom borders. Backend mouse
+// events remain in that 640x480 coordinate space, so menu hit testing must undo
+// the presentation transform.
+static Common::Point presentationToWBASEPoint(const Common::Point &point) {
+ return Common::Point(point.x / 2, (point.y - 30) / 2);
+}
+
static const char *const kBaseSounds[] = {
nullptr,
nullptr,
@@ -122,7 +131,8 @@ BaseGame::BaseGame(HopkinsEngine *vm) :
_shortcutKeymapWasEnabled(false), _baseKeymapWasEnabled(false), _wbaseEnhancementsKeymapWasEnabled(false),
_inputSuspended(false), _mainMenuRequested(false),
_presentationRefreshRequested(false), _timingResetRequested(false),
- _wbaseEnhancementsNavigationMapVisible(false), _quitRequested(false) {
+ _wbaseEnhancementsNavigationMapVisible(false), _wbaseEnhancementsAutoplayMouseVisible(false),
+ _quitRequested(false) {
_framebuffer.resize(kBaseFrameWidth * kBaseFrameHeight);
Common::fill(_audioLoaded, _audioLoaded + ARRAYSIZE(_audioLoaded), false);
}
@@ -193,6 +203,8 @@ BaseRunResult BaseGame::run(int entryId) {
_presentationRefreshRequested = false;
_timingResetRequested = false;
_wbaseEnhancementsNavigationMapVisible = false;
+ _wbaseEnhancementsAutoplay.reset();
+ _wbaseEnhancementsAutoplayMouseVisible = false;
_quitRequested = false;
_result = -1;
renderFrame();
@@ -212,11 +224,18 @@ BaseRunResult BaseGame::run(int entryId) {
}
if (_result != -1 || _quitRequested || _vm->shouldQuit())
break;
- if (_inputSuspended || _wbaseEnhancementsNavigationMapVisible) {
+ if (_inputSuspended || _wbaseEnhancementsNavigationMapVisible || _wbaseEnhancementsAutoplay.menuVisible()) {
previousTime = now;
accumulator = 0;
+ if (_wbaseEnhancementsAutoplay.menuChoicesVisible(now)) {
+ const Common::Point mouse = presentationToWBASEPoint(g_system->getEventManager()->getMousePos());
+ _wbaseEnhancementsAutoplay.updateMenuPointer(mouse.x, mouse.y);
+ setAutoplayMenuMouseVisible(true);
+ }
+ if ((_wbaseEnhancementsNavigationMapVisible || _wbaseEnhancementsAutoplay.menuVisible()) && !_inputSuspended)
+ renderFrame();
_vm->_soundMan->checkSounds();
- g_system->delayMillis(10);
+ g_system->delayMillis((_wbaseEnhancementsNavigationMapVisible || _wbaseEnhancementsAutoplay.menuVisible()) ? 50 : 10);
continue;
}
const uint32 elapsed = MIN<uint32>(now - previousTime, 250);
@@ -226,7 +245,17 @@ BaseRunResult BaseGame::run(int entryId) {
bool advanced = false;
while (accumulator >= 1000 && _result == -1) {
accumulator -= 1000;
- const int tickResult = _engine->tick(_input);
+ BaseInputState autoplayInput;
+ BaseInputState *tickInput = &_input;
+ if (_wbaseEnhancementsAutoplay.active()) {
+ const int forcedRoom = _wbaseEnhancementsAutoplay.update(*_engine, autoplayInput);
+ if (forcedRoom >= 94 && forcedRoom <= 99) {
+ _result = forcedRoom;
+ break;
+ }
+ tickInput = &autoplayInput;
+ }
+ const int tickResult = _engine->tick(*tickInput);
processSoundEvents();
if (tickResult >= 0)
_result = tickResult;
@@ -419,6 +448,29 @@ void BaseGame::pollInput() {
_input = BaseInputState();
_timingResetRequested = true;
break;
+ case Common::EVENT_KEYDOWN:
+ if (_wbaseEnhancementsAutoplay.menuChoicesVisible(g_system->getMillis()) &&
+ (event.kbd.keycode == Common::KEYCODE_RETURN ||
+ event.kbd.keycode == Common::KEYCODE_KP_ENTER)) {
+ _wbaseEnhancementsAutoplay.startSelected(*_engine);
+ setAutoplayMenuMouseVisible(false);
+ _input = BaseInputState();
+ _timingResetRequested = true;
+ renderFrame();
+ }
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ if (_wbaseEnhancementsAutoplay.menuChoicesVisible(g_system->getMillis())) {
+ const Common::Point mouse = presentationToWBASEPoint(event.mouse);
+ if (!_wbaseEnhancementsAutoplay.selectMenuPointer(mouse.x, mouse.y))
+ break;
+ _wbaseEnhancementsAutoplay.startSelected(*_engine);
+ setAutoplayMenuMouseVisible(false);
+ _input = BaseInputState();
+ _timingResetRequested = true;
+ renderFrame();
+ }
+ break;
default:
break;
}
@@ -431,6 +483,51 @@ void BaseGame::pollInput() {
}
void BaseGame::handleAction(uint32 action, bool pressed) {
+ if (_wbaseEnhancementsAutoplay.menuVisible()) {
+ if (!pressed)
+ return;
+ switch (action) {
+ case kActionBaseForward:
+ _wbaseEnhancementsAutoplay.selectPrevious();
+ break;
+ case kActionBaseBackward:
+ _wbaseEnhancementsAutoplay.selectNext();
+ break;
+ case kActionBaseFire:
+ case kActionBaseUse:
+ _wbaseEnhancementsAutoplay.startSelected(*_engine);
+ setAutoplayMenuMouseVisible(false);
+ break;
+ case kActionBaseMenu:
+ case kActionWBASEEnhancementsAutoplay:
+ _wbaseEnhancementsAutoplay.closeMenu();
+ setAutoplayMenuMouseVisible(false);
+ break;
+ default:
+ return;
+ }
+ _input = BaseInputState();
+ _timingResetRequested = true;
+ renderFrame();
+ return;
+ }
+
+ if (pressed && _wbaseEnhancementsAutoplay.active()) {
+ switch (action) {
+ case kActionBaseForward:
+ case kActionBaseBackward:
+ case kActionBaseTurnLeft:
+ case kActionBaseTurnRight:
+ case kActionBaseFire:
+ case kActionBaseUse:
+ _wbaseEnhancementsAutoplay.cancel();
+ _input = BaseInputState();
+ break;
+ default:
+ break;
+ }
+ }
+
switch (action) {
case kActionBaseForward:
_input.forward = pressed;
@@ -475,11 +572,33 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
renderFrame();
}
break;
+ case kActionWBASEEnhancementsAutoplay:
+ if (pressed && _wbaseEnhancements.enabled()) {
+ _wbaseEnhancementsNavigationMapVisible = false;
+ setAutoplayMenuMouseVisible(false);
+ _wbaseEnhancementsAutoplay.openMenu(g_system->getMillis());
+ _input = BaseInputState();
+ _timingResetRequested = true;
+ renderFrame();
+ }
+ break;
default:
break;
}
}
+void BaseGame::setAutoplayMenuMouseVisible(bool visible) {
+ if (_wbaseEnhancementsAutoplayMouseVisible == visible || !_vm->_events)
+ return;
+ _wbaseEnhancementsAutoplayMouseVisible = visible;
+ if (visible) {
+ _vm->_events->changeMouseCursor(0);
+ _vm->_events->mouseOn();
+ } else {
+ _vm->_events->mouseOff();
+ }
+}
+
void BaseGame::openMainMenu() {
_mainMenuRequested = false;
_input = BaseInputState();
@@ -523,6 +642,8 @@ void BaseGame::renderFrame() {
_wbaseEnhancements.renderNavigationMap(_data, *_engine, _framebuffer.begin());
else
_renderer->render(*_engine, _framebuffer.begin());
+ _wbaseEnhancementsAutoplay.render(_data, _engine->entryReturnId(), _framebuffer.begin(), g_system->getMillis(),
+ !_wbaseEnhancementsNavigationMapVisible);
Common::copy(_framebuffer.begin(), _framebuffer.end(), _vm->_graphicsMan->_frontBuffer);
_vm->_graphicsMan->addDirtyRect(0, 0, kBaseFrameWidth, kBaseFrameHeight);
_vm->_graphicsMan->updateScreen();
diff --git a/engines/hopkins/base.h b/engines/hopkins/base.h
index a52cde6ef7c..3b29ff8b88a 100644
--- a/engines/hopkins/base.h
+++ b/engines/hopkins/base.h
@@ -27,6 +27,7 @@
#include "hopkins/base_data.h"
#include "hopkins/base_engine.h"
#include "hopkins/base_enhancements.h"
+#include "hopkins/base_enhancements_autoplay.h"
#include "common/array.h"
#include "common/scummsys.h"
@@ -91,12 +92,14 @@ private:
void handleAction(uint32 action, bool pressed);
void openMainMenu();
void refreshPresentation();
+ void setAutoplayMenuMouseVisible(bool visible);
void processSoundEvents();
void renderFrame();
void setEngineActive(bool active);
HopkinsEngine *_vm;
WBASEEnhancements _wbaseEnhancements;
+ WBASEEnhancementsAutoplay _wbaseEnhancementsAutoplay;
BaseData _data;
BaseEngine *_engine;
BaseRenderer *_renderer;
@@ -115,6 +118,7 @@ private:
bool _presentationRefreshRequested;
bool _timingResetRequested;
bool _wbaseEnhancementsNavigationMapVisible;
+ bool _wbaseEnhancementsAutoplayMouseVisible;
bool _quitRequested;
};
diff --git a/engines/hopkins/base_enhancements.cpp b/engines/hopkins/base_enhancements.cpp
index 613519466a5..b97ab776f9d 100644
--- a/engines/hopkins/base_enhancements.cpp
+++ b/engines/hopkins/base_enhancements.cpp
@@ -24,9 +24,15 @@
#include "hopkins/base_data.h"
#include "hopkins/base_engine.h"
#include "hopkins/base_types.h"
+#include "hopkins/base_enhancements_autoplay.h"
#include "common/config-manager.h"
+#include "common/translation.h"
#include "common/util.h"
+#include "graphics/font.h"
+#include "graphics/fontman.h"
+#include "graphics/pixelformat.h"
+#include "graphics/surface.h"
namespace Hopkins {
@@ -76,69 +82,218 @@ static void drawLine(byte *framebuffer, int x0, int y0, int x1, int y1, byte col
}
}
-} // End of anonymous namespace
+static void fillRect(byte *framebuffer, int left, int top, int right, int bottom, byte color) {
+ for (int y = top; y <= bottom; ++y)
+ for (int x = left; x <= right; ++x)
+ drawPixel(framebuffer, x, y, color);
+}
-WBASEEnhancements::WBASEEnhancements(const Common::String &targetName) :
- _enabled(ConfMan.getBool(kWBASEEnhancementsConfigKey, targetName)) {
+static int triangleEdge(int x0, int y0, int x1, int y1, int x, int y) {
+ return (x - x0) * (y1 - y0) - (y - y0) * (x1 - x0);
}
-void WBASEEnhancements::renderNavigationMap(const BaseData &data, const BaseEngine &engine, byte *framebuffer) const {
- if (!framebuffer)
+static void fillTriangle(byte *framebuffer, int x0, int y0, int x1, int y1,
+ int x2, int y2, byte color) {
+ const int left = MIN(x0, MIN(x1, x2));
+ const int right = MAX(x0, MAX(x1, x2));
+ const int top = MIN(y0, MIN(y1, y2));
+ const int bottom = MAX(y0, MAX(y1, y2));
+ for (int y = top; y <= bottom; ++y) {
+ for (int x = left; x <= right; ++x) {
+ const int edge0 = triangleEdge(x0, y0, x1, y1, x, y);
+ const int edge1 = triangleEdge(x1, y1, x2, y2, x, y);
+ const int edge2 = triangleEdge(x2, y2, x0, y0, x, y);
+ if ((edge0 >= 0 && edge1 >= 0 && edge2 >= 0) ||
+ (edge0 <= 0 && edge1 <= 0 && edge2 <= 0))
+ drawPixel(framebuffer, x, y, color);
+ }
+ }
+}
+
+static void drawPlayerArrow(const BaseData &data, byte *framebuffer, int centerX, int centerY,
+ int angle, byte fillColor, byte outlineColor) {
+ const int32 directionX = data.cosQ16(angle);
+ const int32 directionY = data.sinQ16(angle);
+ const int tipX = centerX + (int)(((int64)directionX * 8) >> kBaseFixedShift);
+ const int tipY = centerY + (int)(((int64)directionY * 8) >> kBaseFixedShift);
+ const int tailX = centerX - (int)(((int64)directionX * 5) >> kBaseFixedShift);
+ const int tailY = centerY - (int)(((int64)directionY * 5) >> kBaseFixedShift);
+ const int wingX = (int)(((int64)-directionY * 5) >> kBaseFixedShift);
+ const int wingY = (int)(((int64)directionX * 5) >> kBaseFixedShift);
+ const int leftX = tailX + wingX;
+ const int leftY = tailY + wingY;
+ const int rightX = tailX - wingX;
+ const int rightY = tailY - wingY;
+
+ fillTriangle(framebuffer, tipX, tipY, leftX, leftY, rightX, rightY, fillColor);
+ drawLine(framebuffer, tipX, tipY, leftX, leftY, outlineColor);
+ drawLine(framebuffer, leftX, leftY, rightX, rightY, outlineColor);
+ drawLine(framebuffer, rightX, rightY, tipX, tipY, outlineColor);
+ drawPixel(framebuffer, centerX, centerY, fillColor);
+}
+
+static void drawSmallDigit(byte *framebuffer, int centerX, int centerY, int digit, byte color) {
+ static const byte patterns[6][5] = {
+ { 2, 6, 2, 2, 7 }, // 1
+ { 7, 1, 7, 4, 7 }, // 2
+ { 7, 1, 7, 1, 7 }, // 3
+ { 5, 5, 7, 1, 1 }, // 4
+ { 7, 4, 7, 1, 7 }, // 5
+ { 7, 4, 7, 5, 7 } // 6
+ };
+ if (digit < 1 || digit > 6)
return;
+ for (int y = 0; y < 5; ++y) {
+ for (int x = 0; x < 3; ++x) {
+ if (patterns[digit - 1][y] & (1 << (2 - x)))
+ drawPixel(framebuffer, centerX - 1 + x, centerY - 2 + y, color);
+ }
+ }
+}
+static void drawExitMarker(byte *framebuffer, int centerX, int centerY, byte backgroundColor,
+ byte exitColor, byte highlightColor, int destinationNumber = 0) {
+ fillRect(framebuffer, centerX - 4, centerY - 4, centerX + 4, centerY + 4, backgroundColor);
+ fillRect(framebuffer, centerX - 3, centerY - 3, centerX + 3, centerY + 3, exitColor);
+ fillRect(framebuffer, centerX - 2, centerY - 2, centerX + 2, centerY + 2, backgroundColor);
+ if (destinationNumber)
+ drawSmallDigit(framebuffer, centerX, centerY, destinationNumber, highlightColor);
+ else
+ drawPixel(framebuffer, centerX, centerY, highlightColor);
+}
+
+static void drawGuardMarker(byte *framebuffer, int centerX, int centerY, byte color) {
+ fillRect(framebuffer, centerX - 1, centerY - 1, centerX + 1, centerY + 1, color);
+}
+
+static void drawOpaqueLegend(const BaseData &data, byte *framebuffer, byte backgroundColor,
+ byte textColor, byte playerColor, byte exitColor, byte guardColor, byte doorColor) {
+ Graphics::Surface surface;
+ surface.init(kBaseFrameWidth, kBaseFrameHeight, kBaseFrameWidth, framebuffer,
+ Graphics::PixelFormat::createFormatCLUT8());
+ const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
+ if (!font)
+ return;
+
+ static const int kSymbolX = 11;
+ static const int kTextX = 22;
+ static const int kTextWidth = 68;
+ static const int kRows[] = { 53, 73, 93, 113 };
+ drawPlayerArrow(data, framebuffer, kSymbolX, kRows[0] + 4, 0, playerColor, textColor);
+ drawExitMarker(framebuffer, kSymbolX, kRows[1] + 4, backgroundColor, exitColor, textColor);
+ drawGuardMarker(framebuffer, kSymbolX, kRows[2] + 4, guardColor);
+ drawLine(framebuffer, kSymbolX - 4, kRows[3] + 4, kSymbolX + 4, kRows[3] + 4, doorColor);
+
+ font->drawString(&surface, _("Player"), kTextX, kRows[0], kTextWidth, textColor);
+ font->drawString(&surface, _("Exit"), kTextX, kRows[1], kTextWidth, textColor);
+ font->drawString(&surface, _("Guard"), kTextX, kRows[2], kTextWidth, textColor);
+ font->drawString(&surface, _("Door"), kTextX, kRows[3], kTextWidth, textColor);
+ font->drawString(&surface, _("Game paused"), 226, 29, 92, textColor,
+ Graphics::kTextAlignCenter);
+ for (int index = 0; index < WBASEEnhancementsAutoplay::destinationCount(); ++index) {
+ font->drawString(&surface, Common::String::format("%d", index + 1), 229, 51 + index * 15,
+ 10, exitColor, Graphics::kTextAlignRight);
+ font->drawString(&surface, WBASEEnhancementsAutoplay::destinationMapLabel(index),
+ 243, 51 + index * 15, 75, textColor);
+ }
+ font->drawString(&surface, _("A: autoplay"), 226, 151, 92, textColor,
+ Graphics::kTextAlignCenter);
+ font->drawString(&surface, _("M/Esc: close map"), 0, 183, kBaseFrameWidth, textColor,
+ Graphics::kTextAlignCenter);
+}
+
+struct NavigationMapColors {
+ byte background;
+ byte wall;
+ byte door;
+ byte exit;
+ byte guard;
+ byte player;
+ byte highlight;
+};
+
+static NavigationMapColors navigationMapColors(const BaseData &data) {
const byte *palette = data.palette();
- const byte backgroundColor = nearestPaletteColor(palette, 0, 0, 0);
- const byte wallColor = nearestPaletteColor(palette, 190, 190, 190);
- const byte doorColor = nearestPaletteColor(palette, 255, 210, 0);
- const byte exitColor = nearestPaletteColor(palette, 0, 220, 255);
- const byte guardColor = nearestPaletteColor(palette, 255, 70, 20);
- const byte playerColor = nearestPaletteColor(palette, 40, 255, 60);
- Common::fill(framebuffer, framebuffer + kBaseFrameWidth * kBaseFrameHeight, backgroundColor);
+ NavigationMapColors colors;
+ colors.background = nearestPaletteColor(palette, 0, 0, 0);
+ colors.wall = nearestPaletteColor(palette, 190, 190, 190);
+ colors.door = nearestPaletteColor(palette, 255, 210, 0);
+ colors.exit = nearestPaletteColor(palette, 0, 220, 255);
+ colors.guard = nearestPaletteColor(palette, 255, 70, 20);
+ colors.player = nearestPaletteColor(palette, 40, 255, 60);
+ colors.highlight = nearestPaletteColor(palette, 255, 255, 255);
+ return colors;
+}
+
+static void drawMapMarkers(const BaseData &data, const BaseEngine &engine, byte *framebuffer,
+ int mapScale, int mapLeft, int mapTop, const NavigationMapColors &colors) {
+ // BASE.MAP contains exactly the six wall cells that WBASE accepts as
+ // transitions when the player faces them and presses Space.
+ for (int mapY = 0; mapY < kBaseMapHeight; ++mapY) {
+ for (int mapX = 0; mapX < kBaseMapWidth; ++mapX) {
+ if ((data.mapCodeXY(mapX, mapY) & 0xff) != kBaseExitBitmap)
+ continue;
+ const int centerX = mapLeft + mapX * mapScale + mapScale / 2;
+ const int centerY = mapTop + mapY * mapScale + mapScale / 2;
+ const int destination = WBASEEnhancementsAutoplay::destinationIndexForExitMapPos(baseMapIndex(mapX, mapY));
+ drawExitMarker(framebuffer, centerX, centerY, colors.background, colors.exit,
+ colors.highlight, destination + 1);
+ }
+ }
+
+ for (int objectId = 1; objectId <= kBaseMaxObjects; ++objectId) {
+ const BaseObject &object = engine.object(objectId);
+ if (!object.active || object.mode == kBaseObjectDead)
+ continue;
+ const int pixelX = mapLeft + (object.x >> kBaseCellShift) * mapScale + mapScale / 2;
+ const int pixelY = mapTop + (object.y >> kBaseCellShift) * mapScale + mapScale / 2;
+ drawGuardMarker(framebuffer, pixelX, pixelY, colors.guard);
+ }
+
+ const int playerX = mapLeft + (engine.playerX() >> kBaseCellShift) * mapScale + mapScale / 2;
+ const int playerY = mapTop + (engine.playerY() >> kBaseCellShift) * mapScale + mapScale / 2;
+ drawPlayerArrow(data, framebuffer, playerX, playerY, engine.playerAngle(), colors.player, colors.highlight);
+}
+
+static void renderNavigationMapOpaque(const BaseData &data, const BaseEngine &engine,
+ byte *framebuffer, const NavigationMapColors &colors) {
+ Common::fill(framebuffer, framebuffer + kBaseFrameWidth * kBaseFrameHeight, colors.background);
static const int kMapScale = 2;
static const int kMapPixelWidth = kBaseMapWidth * kMapScale;
static const int kMapPixelHeight = kBaseMapHeight * kMapScale;
const int mapLeft = (kBaseFrameWidth - kMapPixelWidth) / 2;
const int mapTop = (kBaseFrameHeight - kMapPixelHeight) / 2;
-
for (int mapY = 0; mapY < kBaseMapHeight; ++mapY) {
for (int mapX = 0; mapX < kBaseMapWidth; ++mapX) {
const uint16 code = data.mapCodeXY(mapX, mapY);
const byte lowCode = code & 0xff;
if (!code || lowCode >= 0xfc)
continue;
-
- byte color = wallColor;
- if (lowCode == kBaseDoorXCode || lowCode == kBaseDoorYCode)
- color = doorColor;
- else if (lowCode == kBaseExitBitmap)
- color = exitColor;
-
+ const byte color = (lowCode == kBaseDoorXCode || lowCode == kBaseDoorYCode) ? colors.door : colors.wall;
const int pixelX = mapLeft + mapX * kMapScale;
const int pixelY = mapTop + mapY * kMapScale;
- for (int y = 0; y < kMapScale; ++y)
- for (int x = 0; x < kMapScale; ++x)
- drawPixel(framebuffer, pixelX + x, pixelY + y, color);
+ fillRect(framebuffer, pixelX, pixelY, pixelX + kMapScale - 1, pixelY + kMapScale - 1, color);
}
}
- for (int objectId = 1; objectId <= kBaseMaxObjects; ++objectId) {
- const BaseObject &object = engine.object(objectId);
- if (!object.active || object.mode == kBaseObjectDead)
- continue;
- const int pixelX = mapLeft + (object.x >> kBaseCellShift) * kMapScale;
- const int pixelY = mapTop + (object.y >> kBaseCellShift) * kMapScale;
- drawPixel(framebuffer, pixelX, pixelY, guardColor);
- }
+ drawMapMarkers(data, engine, framebuffer, kMapScale, mapLeft, mapTop, colors);
+ drawOpaqueLegend(data, framebuffer, colors.background, colors.highlight, colors.player,
+ colors.exit, colors.guard, colors.door);
+}
+
+} // End of anonymous namespace
+
+WBASEEnhancements::WBASEEnhancements(const Common::String &targetName) :
+ _enabled(ConfMan.getBool(kWBASEEnhancementsConfigKey, targetName)) {
+}
+
+void WBASEEnhancements::renderNavigationMap(const BaseData &data, const BaseEngine &engine, byte *framebuffer) const {
+ if (!framebuffer)
+ return;
- const int playerX = mapLeft + (engine.playerX() >> kBaseCellShift) * kMapScale;
- const int playerY = mapTop + (engine.playerY() >> kBaseCellShift) * kMapScale;
- for (int y = -2; y <= 2; ++y)
- for (int x = -2; x <= 2; ++x)
- drawPixel(framebuffer, playerX + x, playerY + y, playerColor);
- const int directionX = playerX + (int)(((int64)data.cosQ16(engine.playerAngle()) * 10) >> kBaseFixedShift);
- const int directionY = playerY + (int)(((int64)data.sinQ16(engine.playerAngle()) * 10) >> kBaseFixedShift);
- drawLine(framebuffer, playerX, playerY, directionX, directionY, playerColor);
+ const NavigationMapColors colors = navigationMapColors(data);
+ renderNavigationMapOpaque(data, engine, framebuffer, colors);
}
} // End of namespace Hopkins
diff --git a/engines/hopkins/base_enhancements.h b/engines/hopkins/base_enhancements.h
index ffe829912d5..67ba2ae83a0 100644
--- a/engines/hopkins/base_enhancements.h
+++ b/engines/hopkins/base_enhancements.h
@@ -39,6 +39,7 @@ public:
bool enabled() const { return _enabled; }
bool navigationMapEnabled() const { return _enabled; }
+ /** Render the readable opaque navigation-map presentation. */
void renderNavigationMap(const BaseData &data, const BaseEngine &engine, byte *framebuffer) const;
private:
diff --git a/engines/hopkins/base_enhancements_autoplay.cpp b/engines/hopkins/base_enhancements_autoplay.cpp
index 6059a281d94..1d360fb48c9 100644
--- a/engines/hopkins/base_enhancements_autoplay.cpp
+++ b/engines/hopkins/base_enhancements_autoplay.cpp
@@ -26,6 +26,7 @@
#include "hopkins/base_types.h"
#include "common/algorithm.h"
+#include "common/debug.h"
#include "common/translation.h"
#include "common/util.h"
#include "graphics/font.h"
@@ -42,24 +43,52 @@ struct AutoplayDestination {
int exitMapPos;
int approachMapPos;
const char *label;
+ const char *mapLabel;
};
// WBASE transition cells and the adjacent cells used to approach them.
static const AutoplayDestination kAutoplayDestinations[] = {
- { 94, 0x0860, 0x0861, "Exit 1: Sector 4" },
- { 95, 0x087a, 0x0879, "Exit 2: Sector 2" },
- { 96, 0x01df, 0x021f, "Exit 3: Office" },
- { 97, 0x05ab, 0x05eb, "Exit 4: Laboratory" },
- { 98, 0x0bab, 0x0b6b, "Exit 5: Resurrection" },
- { 99, 0x01fb, 0x023b, "Exit 6: Bernie's room" }
+ { 94, 0x0860, 0x0861, _s("Exit 1: Containers room"), _s("Containers") },
+ { 95, 0x087a, 0x0879, _s("Exit 2: Turbines room"), _s("Turbines") },
+ { 96, 0x01df, 0x021f, _s("Exit 3: Office"), _s("Office") },
+ { 97, 0x05ab, 0x05eb, _s("Exit 4: Laboratory"), _s("Laboratory") },
+ { 98, 0x0bab, 0x0b6b, _s("Exit 5: Resurrection room"), _s("Resurrection") },
+ { 99, 0x01fb, 0x023b, _s("Exit 6: Dome"), _s("Dome") }
};
static const int kAutoplayDestinationCount = ARRAYSIZE(kAutoplayDestinations);
static const int kAutoplayAimTolerance = 24;
static const int kAutoplayMoveTolerance = 48;
static const int kAutoplayWaypointTolerance = 12;
-static const int kAutoplayCombatDistance = 600;
-static const int kAutoplayStuckTicks = 24;
+// Keep a four-unit clearance from diagonal corner blockers.
+static const int kAutoplayCornerLaneHalfWidth = 4;
+static const int kAutoplayCornerLaneBias = 16;
+static const int kAutoplayInitialCombatDistance = 600;
+static const int kAutoplayNearbyCombatDistance = 128;
+static const int kAutoplayFinalCombatHalfAngle = 240;
+// Reach exit-first combat priority before the 100-second route limit.
+static const int kAutoplayCombatPriorityTicks = 60 * 24;
+// At 24 Hz, 60 ticks permits a 180-degree turn before recovery.
+static const int kAutoplayStuckTicks = 60;
+static const int kAutoplayMaximumTicks = 100 * 24;
+static const int kAutoplayRecoveryReverseTicks = 6;
+static const int kAutoplayRecoveryTurnTicks = 12;
+static const int kAutoplayRecoveryAdvanceTicks = 8;
+static const uint32 kAutoplayMenuDelayMs = 1000;
+static const int kAutoplayMenuLeft = 42;
+static const int kAutoplayMenuTop = 25;
+static const int kAutoplayMenuRight = 277;
+static const int kAutoplayMenuBottom = 180;
+static const int kAutoplayMenuChoiceTop = 62;
+static const int kAutoplayMenuChoiceSpacing = 14;
+static const int kAutoplayMenuChoiceHeight = 11;
+
+enum AutoplayRecoveryPhase {
+ kAutoplayRecoveryNone,
+ kAutoplayRecoveryReverse,
+ kAutoplayRecoveryTurn,
+ kAutoplayRecoveryAdvance
+};
static int absoluteValue(int value) {
return value < 0 ? -value : value;
@@ -98,6 +127,62 @@ static bool canStep(const BaseEngine &engine, int from, int to) {
return false;
}
+static bool blockingGridCode(const uint16 *grid, int mapPos) {
+ if (mapPos < 0 || mapPos >= kBaseAckGridArray)
+ return true;
+ const uint16 code = grid[mapPos];
+ return code && !(code & kBaseWallPass);
+}
+
+static bool blockingCorner(const BaseEngine &engine, int xGridPos, int yGridPos) {
+ return blockingGridCode(engine.xGrid(), xGridPos) || blockingGridCode(engine.yGrid(), yGridPos);
+}
+
+static void routeCornerLaneBias(const BaseEngine &engine, int from, int to, int &biasX, int &biasY) {
+ biasX = 0;
+ biasY = 0;
+ const int centerX = (from & 63) * kBaseCellSize + kBaseCellSize / 2;
+ const int centerY = (from >> 6) * kBaseCellSize + kBaseCellSize / 2;
+ if (to == from + 1) {
+ if (engine.playerY() < centerY - kAutoplayCornerLaneHalfWidth &&
+ blockingCorner(engine, from + 1 - kBaseMapWidth, from + 1))
+ biasY = kAutoplayCornerLaneBias;
+ else if (engine.playerY() > centerY + kAutoplayCornerLaneHalfWidth &&
+ blockingCorner(engine, from + 1 + kBaseMapWidth, from + 1 + kBaseMapWidth))
+ biasY = -kAutoplayCornerLaneBias;
+ } else if (to == from - 1) {
+ if (engine.playerY() < centerY - kAutoplayCornerLaneHalfWidth &&
+ blockingCorner(engine, from - kBaseMapWidth, from - 1))
+ biasY = kAutoplayCornerLaneBias;
+ else if (engine.playerY() > centerY + kAutoplayCornerLaneHalfWidth &&
+ blockingCorner(engine, from + kBaseMapWidth, from - 1 + kBaseMapWidth))
+ biasY = -kAutoplayCornerLaneBias;
+ } else if (to == from + kBaseMapWidth) {
+ if (engine.playerX() < centerX - kAutoplayCornerLaneHalfWidth &&
+ blockingCorner(engine, from + kBaseMapWidth, from - 1 + kBaseMapWidth))
+ biasX = kAutoplayCornerLaneBias;
+ else if (engine.playerX() > centerX + kAutoplayCornerLaneHalfWidth &&
+ blockingCorner(engine, from + 1 + kBaseMapWidth, from + 1 + kBaseMapWidth))
+ biasX = -kAutoplayCornerLaneBias;
+ } else if (to == from - kBaseMapWidth) {
+ if (engine.playerX() < centerX - kAutoplayCornerLaneHalfWidth &&
+ blockingCorner(engine, from - kBaseMapWidth, from - 1))
+ biasX = kAutoplayCornerLaneBias;
+ else if (engine.playerX() > centerX + kAutoplayCornerLaneHalfWidth &&
+ blockingCorner(engine, from + 1 - kBaseMapWidth, from + 1))
+ biasX = -kAutoplayCornerLaneBias;
+ }
+}
+
+static int blockingObjectAt(const BaseEngine &engine, int mapPos) {
+ for (int id = 1; id <= kBaseMaxObjects; ++id) {
+ const BaseObject &object = engine.object(id);
+ if (object.active && !object.passable && object.mapPos == mapPos)
+ return id;
+ }
+ return 0;
+}
+
static byte nearestPaletteColor(const byte *palette, int red, int green, int blue) {
int bestDistance = 0x7fffffff;
byte bestColor = 0;
@@ -132,6 +217,13 @@ static void drawFrame(byte *framebuffer, int left, int top, int right, int botto
fillRect(framebuffer, right, top, right, bottom, color);
}
+static Common::String menuDestinationLabel(int destination, int returnRoomId) {
+ Common::String label(kAutoplayDestinations[destination].label);
+ if (kAutoplayDestinations[destination].roomId == returnRoomId)
+ label += " (return)";
+ return label;
+}
+
} // End of anonymous namespace
WBASEEnhancementsAutoplay::WBASEEnhancementsAutoplay() {
@@ -141,7 +233,9 @@ WBASEEnhancementsAutoplay::WBASEEnhancementsAutoplay() {
void WBASEEnhancementsAutoplay::reset() {
_active = false;
_menuVisible = false;
+ _menuOpenedAt = 0;
_selectedDestination = 0;
+ _hoveredDestination = -1;
_destination = -1;
_route.clear();
_routeIndex = 0;
@@ -149,22 +243,56 @@ void WBASEEnhancementsAutoplay::reset() {
_lastPlayerY = -1;
_stuckTicks = 0;
_routeInvalidated = false;
+ _blockedRouteCell = -1;
+ _routeBlocker = 0;
+ _movementBlocker = 0;
+ _activeTicks = 0;
+ _recoveryPhase = kAutoplayRecoveryNone;
+ _recoveryTicks = 0;
+ _recoveryAttempts = 0;
+ _recoveryTurnRight = true;
+}
+
+bool WBASEEnhancementsAutoplay::menuChoicesVisible(uint32 now) const {
+ return _menuVisible && now - _menuOpenedAt >= kAutoplayMenuDelayMs;
}
-void WBASEEnhancementsAutoplay::openMenu() {
+void WBASEEnhancementsAutoplay::openMenu(uint32 now) {
_menuVisible = true;
+ _menuOpenedAt = now;
+ _hoveredDestination = -1;
}
void WBASEEnhancementsAutoplay::closeMenu() {
_menuVisible = false;
+ _hoveredDestination = -1;
}
void WBASEEnhancementsAutoplay::selectPrevious() {
_selectedDestination = (_selectedDestination + kAutoplayDestinationCount - 1) % kAutoplayDestinationCount;
+ _hoveredDestination = -1;
}
void WBASEEnhancementsAutoplay::selectNext() {
_selectedDestination = (_selectedDestination + 1) % kAutoplayDestinationCount;
+ _hoveredDestination = -1;
+}
+
+void WBASEEnhancementsAutoplay::updateMenuPointer(int x, int y) {
+ _hoveredDestination = -1;
+ if (x < kAutoplayMenuLeft || x > kAutoplayMenuRight || y < kAutoplayMenuChoiceTop)
+ return;
+ const int destination = (y - kAutoplayMenuChoiceTop) / kAutoplayMenuChoiceSpacing;
+ const int lineY = kAutoplayMenuChoiceTop + destination * kAutoplayMenuChoiceSpacing;
+ if (destination >= 0 && destination < kAutoplayDestinationCount && y < lineY + kAutoplayMenuChoiceHeight) {
+ _hoveredDestination = destination;
+ _selectedDestination = destination;
+ }
+}
+
+bool WBASEEnhancementsAutoplay::selectMenuPointer(int x, int y) {
+ updateMenuPointer(x, y);
+ return _hoveredDestination >= 0;
}
void WBASEEnhancementsAutoplay::startSelected(const BaseEngine &engine) {
@@ -175,18 +303,67 @@ void WBASEEnhancementsAutoplay::startSelected(const BaseEngine &engine) {
_lastPlayerY = engine.playerY();
_stuckTicks = 0;
_routeInvalidated = false;
- rebuildRoute(engine);
+ _blockedRouteCell = -1;
+ _routeBlocker = 0;
+ _movementBlocker = 0;
+ _activeTicks = 0;
+ _recoveryPhase = kAutoplayRecoveryNone;
+ _recoveryTicks = 0;
+ _recoveryAttempts = 0;
+ if (!rebuildRoute(engine, false)) {
+ warning("Hopkins WBASE enhancement autoplay: no route to room %d",
+ kAutoplayDestinations[_destination].roomId);
+ cancel();
+ } else {
+ debug(1, "Hopkins WBASE enhancement autoplay: room %d, route %u cells",
+ kAutoplayDestinations[_destination].roomId, _route.size());
+ }
}
void WBASEEnhancementsAutoplay::cancel() {
_active = false;
_menuVisible = false;
+ _hoveredDestination = -1;
_destination = -1;
_route.clear();
_routeIndex = 0;
+ _blockedRouteCell = -1;
+ _routeBlocker = 0;
+ _movementBlocker = 0;
+ _activeTicks = 0;
+ _recoveryPhase = kAutoplayRecoveryNone;
+ _recoveryTicks = 0;
+}
+
+int WBASEEnhancementsAutoplay::destinationCount() {
+ return kAutoplayDestinationCount;
+}
+
+int WBASEEnhancementsAutoplay::destinationRoomId(int index) {
+ return index >= 0 && index < kAutoplayDestinationCount ? kAutoplayDestinations[index].roomId : 0;
+}
+
+int WBASEEnhancementsAutoplay::destinationExitMapPos(int index) {
+ return index >= 0 && index < kAutoplayDestinationCount ? kAutoplayDestinations[index].exitMapPos : -1;
+}
+
+const char *WBASEEnhancementsAutoplay::destinationLabel(int index) {
+ return index >= 0 && index < kAutoplayDestinationCount ? kAutoplayDestinations[index].label : "";
+}
+
+const char *WBASEEnhancementsAutoplay::destinationMapLabel(int index) {
+ return index >= 0 && index < kAutoplayDestinationCount ? kAutoplayDestinations[index].mapLabel : "";
+}
+
+int WBASEEnhancementsAutoplay::destinationIndexForExitMapPos(int mapPos) {
+ for (int index = 0; index < kAutoplayDestinationCount; ++index) {
+ if (kAutoplayDestinations[index].exitMapPos == mapPos)
+ return index;
+ }
+ return -1;
}
-bool WBASEEnhancementsAutoplay::rebuildRoute(const BaseEngine &engine) {
+bool WBASEEnhancementsAutoplay::rebuildRoute(const BaseEngine &engine, bool avoidBlockingObjects) {
_route.clear();
_routeIndex = 0;
_routeInvalidated = false;
@@ -198,29 +375,35 @@ bool WBASEEnhancementsAutoplay::rebuildRoute(const BaseEngine &engine) {
if (start < 0 || start >= kBaseMapCellCount || goal < 0 || goal >= kBaseMapCellCount)
return false;
+ static const int kOffsets[] = { 1, -1, kBaseMapWidth, -kBaseMapWidth };
int parent[kBaseMapCellCount];
int queue[kBaseMapCellCount];
- Common::fill(parent, parent + ARRAYSIZE(parent), -2);
- int queueBegin = 0;
- int queueEnd = 0;
- queue[queueEnd++] = start;
- parent[start] = -1;
-
- static const int kOffsets[] = { 1, -1, kBaseMapWidth, -kBaseMapWidth };
- while (queueBegin < queueEnd && parent[goal] == -2) {
- const int current = queue[queueBegin++];
- for (uint direction = 0; direction < ARRAYSIZE(kOffsets); ++direction) {
- const int next = current + kOffsets[direction];
- if (next < 0 || next >= kBaseMapCellCount || parent[next] != -2)
- continue;
- if (!canStep(engine, current, next))
- continue;
- parent[next] = current;
- queue[queueEnd++] = next;
+ bool found = false;
+ // Retry around a current blocker, but ignore moving guards on initial routes.
+ for (int avoidObjects = avoidBlockingObjects ? 1 : 0; avoidObjects >= 0 && !found; --avoidObjects) {
+ Common::fill(parent, parent + ARRAYSIZE(parent), -2);
+ int queueBegin = 0;
+ int queueEnd = 0;
+ queue[queueEnd++] = start;
+ parent[start] = -1;
+ while (queueBegin < queueEnd && parent[goal] == -2) {
+ const int current = queue[queueBegin++];
+ for (uint direction = 0; direction < ARRAYSIZE(kOffsets); ++direction) {
+ const int next = current + kOffsets[direction];
+ if (next < 0 || next >= kBaseMapCellCount || parent[next] != -2)
+ continue;
+ if (!canStep(engine, current, next))
+ continue;
+ if (avoidObjects && next != goal && blockingObjectAt(engine, next))
+ continue;
+ parent[next] = current;
+ queue[queueEnd++] = next;
+ }
}
+ found = parent[goal] != -2;
}
- if (parent[goal] == -2)
+ if (!found)
return false;
Common::Array<int> reverseRoute;
@@ -233,19 +416,30 @@ bool WBASEEnhancementsAutoplay::rebuildRoute(const BaseEngine &engine) {
}
int WBASEEnhancementsAutoplay::findVisibleGuard(const BaseEngine &engine) const {
+ const int priorityTicks = MIN(_activeTicks, kAutoplayCombatPriorityTicks);
+ const int combatDistance = kAutoplayInitialCombatDistance -
+ (kAutoplayInitialCombatDistance - kAutoplayNearbyCombatDistance) *
+ priorityTicks / kAutoplayCombatPriorityTicks;
+ const int combatHalfAngle = kBaseHalfTurn -
+ (kBaseHalfTurn - kAutoplayFinalCombatHalfAngle) *
+ priorityTicks / kAutoplayCombatPriorityTicks;
int bestGuard = 0;
- int bestDistance = kAutoplayCombatDistance + 1;
+ int bestDistance = combatDistance + 1;
for (int id = 1; id <= kBaseMaxObjects; ++id) {
const BaseObject &object = engine.object(id);
if (!object.active || object.passable || object.mode > kBaseObjectFlee)
continue;
const int distance = engine.objectDistance(object);
- if (distance > kAutoplayCombatDistance || distance >= bestDistance)
+ if (distance > combatDistance || distance >= bestDistance)
continue;
const int angle = engine.objectAngle(object.x - engine.playerX(), object.y - engine.playerY());
+ const int angleDelta = absoluteValue(signedAngleDelta(engine.playerAngle(), angle));
+ if (distance > kAutoplayNearbyCombatDistance && angleDelta > combatHalfAngle)
+ continue;
const BaseRayHit wall = engine.castRay(angle, kBaseViewHalfWidth);
- if (wall.hit && wall.distance + 24 < distance)
+ // A visibility margin can expose guards behind thin partitions.
+ if (wall.hit && wall.distance <= distance)
continue;
bestGuard = id;
bestDistance = distance;
@@ -268,29 +462,85 @@ void WBASEEnhancementsAutoplay::updateStuckState(const BaseEngine &engine, bool
if (!triedToMove || engine.playerX() != _lastPlayerX || engine.playerY() != _lastPlayerY) {
_stuckTicks = 0;
} else if (++_stuckTicks >= kAutoplayStuckTicks) {
- _routeInvalidated = true;
+ beginRecovery();
_stuckTicks = 0;
}
_lastPlayerX = engine.playerX();
_lastPlayerY = engine.playerY();
}
-void WBASEEnhancementsAutoplay::update(const BaseEngine &engine, BaseInputState &input) {
+void WBASEEnhancementsAutoplay::beginRecovery() {
+ _recoveryPhase = kAutoplayRecoveryReverse;
+ _recoveryTicks = kAutoplayRecoveryReverseTicks;
+ _recoveryTurnRight = (_recoveryAttempts++ & 1) == 0;
+ _routeInvalidated = true;
+ debug(1, "Hopkins WBASE enhancement autoplay: zero progress; recovery %d turning %s",
+ _recoveryAttempts, _recoveryTurnRight ? "right" : "left");
+}
+
+void WBASEEnhancementsAutoplay::updateRecovery(const BaseEngine &engine, BaseInputState &input) {
+ if (_recoveryPhase == kAutoplayRecoveryReverse) {
+ input.backward = true;
+ } else if (_recoveryPhase == kAutoplayRecoveryTurn) {
+ input.turnRight = _recoveryTurnRight;
+ input.turnLeft = !_recoveryTurnRight;
+ } else if (_recoveryPhase == kAutoplayRecoveryAdvance) {
+ input.forward = true;
+ }
+
+ if (--_recoveryTicks <= 0) {
+ if (_recoveryPhase == kAutoplayRecoveryReverse) {
+ _recoveryPhase = kAutoplayRecoveryTurn;
+ _recoveryTicks = kAutoplayRecoveryTurnTicks;
+ } else if (_recoveryPhase == kAutoplayRecoveryTurn) {
+ _recoveryPhase = kAutoplayRecoveryAdvance;
+ _recoveryTicks = kAutoplayRecoveryAdvanceTicks;
+ } else {
+ _recoveryPhase = kAutoplayRecoveryNone;
+ _recoveryTicks = 0;
+ _routeInvalidated = true;
+ }
+ }
+ _lastPlayerX = engine.playerX();
+ _lastPlayerY = engine.playerY();
+}
+
+int WBASEEnhancementsAutoplay::update(const BaseEngine &engine, BaseInputState &input) {
input = BaseInputState();
+ _routeBlocker = 0;
+ _movementBlocker = 0;
if (!_active || _menuVisible || _destination < 0 || _destination >= kAutoplayDestinationCount)
- return;
+ return -1;
+
+ // Complete after 100 active seconds so forced autoplay cannot remain stuck.
+ // Paused time is excluded because update() is not called.
+ if (++_activeTicks >= kAutoplayMaximumTicks) {
+ const int roomId = kAutoplayDestinations[_destination].roomId;
+ warning("Hopkins WBASE enhancement autoplay: forcing room %d after %d active ticks",
+ roomId, _activeTicks);
+ cancel();
+ return roomId;
+ }
+
+ const int playerMapPos = baseWorldMapIndex(engine.playerX(), engine.playerY());
+ _movementBlocker = blockingObjectAt(engine, playerMapPos);
+
+ if (_recoveryPhase != kAutoplayRecoveryNone) {
+ updateRecovery(engine, input);
+ return -1;
+ }
const int guardId = findVisibleGuard(engine);
if (guardId) {
const BaseObject &guard = engine.object(guardId);
if (aimAt(engine, guard.x, guard.y, input) && engine.weaponCounter() == 0)
input.fire = true;
- updateStuckState(engine, false);
- return;
+ // Fighting without movement also advances stall recovery.
+ updateStuckState(engine, true);
+ return -1;
}
const AutoplayDestination &destination = kAutoplayDestinations[_destination];
- const int playerMapPos = baseWorldMapIndex(engine.playerX(), engine.playerY());
if (playerMapPos == destination.approachMapPos) {
const int exitX = (destination.exitMapPos & 63) * kBaseCellSize + kBaseCellSize / 2;
const int exitY = (destination.exitMapPos >> 6) * kBaseCellSize + kBaseCellSize / 2;
@@ -301,19 +551,19 @@ void WBASEEnhancementsAutoplay::update(const BaseEngine &engine, BaseInputState
const bool aligned = aimAt(engine, approachX, approachY, input);
input.forward = aligned;
updateStuckState(engine, input.forward);
- return;
+ return -1;
}
if (aimAt(engine, exitX, exitY, input))
input.exitRequested = true;
updateStuckState(engine, false);
- return;
+ return -1;
}
if (_routeInvalidated || _route.empty() || _routeIndex >= _route.size())
- rebuildRoute(engine);
+ rebuildRoute(engine, false);
if (_route.empty()) {
cancel();
- return;
+ return -1;
}
while (_routeIndex < _route.size() && playerMapPos == _route[_routeIndex]) {
@@ -328,14 +578,49 @@ void WBASEEnhancementsAutoplay::update(const BaseEngine &engine, BaseInputState
if (_routeIndex >= _route.size()) {
_routeInvalidated = true;
updateStuckState(engine, false);
- return;
+ return -1;
+ }
+
+ const int routeBlocker = blockingObjectAt(engine, _route[_routeIndex]);
+ _routeBlocker = routeBlocker;
+ if (!routeBlocker)
+ _blockedRouteCell = -1;
+ if (routeBlocker && _route[_routeIndex] != destination.approachMapPos &&
+ _route[_routeIndex] != _blockedRouteCell) {
+ const int oldTarget = _route[_routeIndex];
+ _blockedRouteCell = oldTarget;
+ _routeInvalidated = true;
+ rebuildRoute(engine, true);
+ debug(2, "Hopkins WBASE enhancement autoplay: tick %d target %04x occupied by guard %d; replanned to %04x",
+ _activeTicks, oldTarget, routeBlocker,
+ _routeIndex < _route.size() ? _route[_routeIndex] : -1);
+ if (_route.empty()) {
+ cancel();
+ return -1;
+ }
}
const int expectedCell = _routeIndex > 0 ? _route[_routeIndex - 1] : _route[0];
if (playerMapPos != expectedCell && playerMapPos != _route[_routeIndex]) {
_routeInvalidated = true;
updateStuckState(engine, false);
- return;
+ return -1;
+ }
+
+ const int routeTarget = _route[_routeIndex];
+ if (playerMapPos == expectedCell) {
+ int biasX = 0;
+ int biasY = 0;
+ routeCornerLaneBias(engine, playerMapPos, routeTarget, biasX, biasY);
+ if (biasX || biasY) {
+ const int forwardX = (routeTarget & 63) - (playerMapPos & 63);
+ const int forwardY = (routeTarget >> 6) - (playerMapPos >> 6);
+ const int steeringX = engine.playerX() + forwardX * kBaseCellSize + biasX;
+ const int steeringY = engine.playerY() + forwardY * kBaseCellSize + biasY;
+ input.forward = aimAt(engine, steeringX, steeringY, input);
+ updateStuckState(engine, input.forward);
+ return -1;
+ }
}
const int waypointX = (_route[_routeIndex] & 63) * kBaseCellSize + kBaseCellSize / 2;
@@ -348,9 +633,11 @@ void WBASEEnhancementsAutoplay::update(const BaseEngine &engine, BaseInputState
input.turnLeft = true;
input.forward = absoluteValue(delta) <= kAutoplayMoveTolerance;
updateStuckState(engine, input.forward);
+ return -1;
}
-void WBASEEnhancementsAutoplay::render(const BaseData &data, byte *framebuffer, bool showStatus) const {
+void WBASEEnhancementsAutoplay::render(const BaseData &data, int returnRoomId,
+ byte *framebuffer, uint32 now, bool showStatus) const {
if (!framebuffer || (!_menuVisible && (!_active || !showStatus)))
return;
@@ -365,37 +652,46 @@ void WBASEEnhancementsAutoplay::render(const BaseData &data, byte *framebuffer,
const byte background = nearestPaletteColor(palette, 0, 0, 0);
const byte border = nearestPaletteColor(palette, 190, 190, 190);
const byte text = nearestPaletteColor(palette, 255, 255, 255);
- const byte selected = nearestPaletteColor(palette, 40, 255, 60);
+ const byte selected = nearestPaletteColor(palette, 255, 220, 0);
if (_menuVisible) {
- static const int kLeft = 42;
- static const int kTop = 27;
- static const int kRight = 277;
- static const int kBottom = 172;
- fillRect(framebuffer, kLeft, kTop, kRight, kBottom, background);
- drawFrame(framebuffer, kLeft, kTop, kRight, kBottom, border);
- font->drawString(&surface, _("WBASE AUTOPLAY"), kLeft + 4, kTop + 9,
- kRight - kLeft - 7, text, Graphics::kTextAlignCenter);
- font->drawString(&surface, _("Fight your way to:"), kLeft + 4, kTop + 27,
- kRight - kLeft - 7, text, Graphics::kTextAlignCenter);
+ if (!menuChoicesVisible(now))
+ return;
+ fillRect(framebuffer, kAutoplayMenuLeft, kAutoplayMenuTop,
+ kAutoplayMenuRight, kAutoplayMenuBottom, background);
+ drawFrame(framebuffer, kAutoplayMenuLeft, kAutoplayMenuTop,
+ kAutoplayMenuRight, kAutoplayMenuBottom, border);
+ font->drawString(&surface, _("WBASE AUTOPLAY"), kAutoplayMenuLeft + 4,
+ kAutoplayMenuTop + 7, kAutoplayMenuRight - kAutoplayMenuLeft - 7,
+ text, Graphics::kTextAlignCenter);
+ font->drawString(&surface, _("Fight your way to:"), kAutoplayMenuLeft + 4,
+ kAutoplayMenuTop + 21, kAutoplayMenuRight - kAutoplayMenuLeft - 7,
+ text, Graphics::kTextAlignCenter);
for (int index = 0; index < kAutoplayDestinationCount; ++index) {
- const Common::U32String label = Common::U32String::format("%c %S",
- index == _selectedDestination ? '>' : ' ', _(kAutoplayDestinations[index].label).c_str());
- font->drawString(&surface, label, kLeft + 18, kTop + 45 + index * 12,
- kRight - kLeft - 35, index == _selectedDestination ? selected : text);
+ const Common::String label = menuDestinationLabel(index, returnRoomId);
+ const bool highlighted = index == (_hoveredDestination >= 0 ? _hoveredDestination : _selectedDestination);
+ font->drawString(&surface, Common::String::format("%c %s", highlighted ? '>' : ' ', label.c_str()),
+ kAutoplayMenuLeft + 14, kAutoplayMenuChoiceTop + index * kAutoplayMenuChoiceSpacing,
+ kAutoplayMenuRight - kAutoplayMenuLeft - 27, highlighted ? selected : text);
}
- font->drawString(&surface, _("Up/Down: choose Space: start Esc: cancel"),
- kLeft + 4, kBottom - 15, kRight - kLeft - 7, text, Graphics::kTextAlignCenter);
+ font->drawString(&surface, _("Up/Down: choose"), kAutoplayMenuLeft + 8,
+ kAutoplayMenuBottom - 25, 104, text);
+ font->drawString(&surface, _("Enter/Space: start"), kAutoplayMenuLeft + 116,
+ kAutoplayMenuBottom - 25, 111, text);
+ font->drawString(&surface, _("Esc/A: cancel"), kAutoplayMenuLeft + 4,
+ kAutoplayMenuBottom - 13, kAutoplayMenuRight - kAutoplayMenuLeft - 7,
+ text, Graphics::kTextAlignCenter);
return;
}
- const Common::U32String status = Common::U32String::format(_("AUTO: %S A: route"),
- _(kAutoplayDestinations[_destination].label).c_str());
+ const int secondsRemaining = MAX(0, (kAutoplayMaximumTicks - _activeTicks + 23) / 24);
+ const Common::String status = Common::String::format("To %s - %ds",
+ kAutoplayDestinations[_destination].label, secondsRemaining);
const int statusWidth = MIN(font->getStringWidth(status) + 10, kBaseFrameWidth - 8);
const int statusLeft = (kBaseFrameWidth - statusWidth) / 2;
fillRect(framebuffer, statusLeft, 2, statusLeft + statusWidth - 1, 14, background);
drawFrame(framebuffer, statusLeft, 2, statusLeft + statusWidth - 1, 14, border);
- font->drawString(&surface, status, statusLeft + 4, 4, statusWidth - 8, selected,
+ font->drawString(&surface, status, statusLeft + 4, 4, statusWidth - 8, text,
Graphics::kTextAlignCenter);
}
diff --git a/engines/hopkins/base_enhancements_autoplay.h b/engines/hopkins/base_enhancements_autoplay.h
index d9ecb9d56f4..3f9b8cd6198 100644
--- a/engines/hopkins/base_enhancements_autoplay.h
+++ b/engines/hopkins/base_enhancements_autoplay.h
@@ -39,27 +39,42 @@ public:
void reset();
bool active() const { return _active; }
bool menuVisible() const { return _menuVisible; }
+ bool menuChoicesVisible(uint32 now) const;
- void openMenu();
+ void openMenu(uint32 now);
void closeMenu();
void selectPrevious();
void selectNext();
+ void updateMenuPointer(int x, int y);
+ bool selectMenuPointer(int x, int y);
void startSelected(const BaseEngine &engine);
void cancel();
- /** Replace the next simulation tick's input with autopilot controls. */
- void update(const BaseEngine &engine, BaseInputState &input);
- void render(const BaseData &data, byte *framebuffer, bool showStatus) const;
+ /** Return the forced destination room, or -1, after updating input. */
+ int update(const BaseEngine &engine, BaseInputState &input);
+ void render(const BaseData &data, int returnRoomId,
+ byte *framebuffer, uint32 now, bool showStatus) const;
+
+ static int destinationCount();
+ static int destinationRoomId(int index);
+ static int destinationExitMapPos(int index);
+ static const char *destinationLabel(int index);
+ static const char *destinationMapLabel(int index);
+ static int destinationIndexForExitMapPos(int mapPos);
private:
- bool rebuildRoute(const BaseEngine &engine);
+ bool rebuildRoute(const BaseEngine &engine, bool avoidBlockingObjects);
int findVisibleGuard(const BaseEngine &engine) const;
bool aimAt(const BaseEngine &engine, int targetX, int targetY, BaseInputState &input) const;
void updateStuckState(const BaseEngine &engine, bool triedToMove);
+ void beginRecovery();
+ void updateRecovery(const BaseEngine &engine, BaseInputState &input);
bool _active;
bool _menuVisible;
+ uint32 _menuOpenedAt;
int _selectedDestination;
+ int _hoveredDestination;
int _destination;
Common::Array<int> _route;
uint _routeIndex;
@@ -67,6 +82,14 @@ private:
int _lastPlayerY;
int _stuckTicks;
bool _routeInvalidated;
+ int _blockedRouteCell;
+ int _routeBlocker;
+ int _movementBlocker;
+ int _activeTicks;
+ int _recoveryPhase;
+ int _recoveryTicks;
+ int _recoveryAttempts;
+ bool _recoveryTurnRight;
};
} // End of namespace Hopkins
diff --git a/engines/hopkins/hopkins.h b/engines/hopkins/hopkins.h
index 895c214437d..75beb062e0b 100644
--- a/engines/hopkins/hopkins.h
+++ b/engines/hopkins/hopkins.h
@@ -76,7 +76,8 @@ enum HOPKINSAction {
kActionBaseUse,
kActionBaseToggleTextures,
kActionBaseMenu,
- kActionWBASEEnhancementsNavigationMap
+ kActionWBASEEnhancementsNavigationMap,
+ kActionWBASEEnhancementsAutoplay
};
enum HopkinsDebugChannels {
diff --git a/engines/hopkins/metaengine.cpp b/engines/hopkins/metaengine.cpp
index 6225bfc8a66..3e7bcb0fb1a 100644
--- a/engines/hopkins/metaengine.cpp
+++ b/engines/hopkins/metaengine.cpp
@@ -70,7 +70,7 @@ static const ADExtraGuiOptionsMap optionsList[] = {
GAMEOPTION_WBASE_ENHANCEMENTS,
{
_s("WBASE enhancements"),
- _s("Enable navigation/usability enhancements for Windows base shooter"),
+ _s("Enable navigation map and autoplay options for Windows base shooter"),
kWBASEEnhancementsConfigKey,
false,
0,
@@ -325,6 +325,11 @@ Common::KeymapArray HopkinsMetaEngine::initKeymaps(const char *target) const {
act->addDefaultInputMapping("m");
wbaseEnhancementsKeyMap->addAction(act);
+ act = new Action("WBASE_ENHANCEMENTS_AUTOPLAY", _("WBASE enhancements: choose autoplay destination"));
+ act->setCustomEngineActionEvent(kActionWBASEEnhancementsAutoplay);
+ act->addDefaultInputMapping("a");
+ wbaseEnhancementsKeyMap->addAction(act);
+
KeymapArray keymaps(4);
keymaps[0] = engineKeyMap;
keymaps[1] = gameKeyMap;
Commit: 01000a967ed2bd835ba892cbf2c7964919aeaae2
https://github.com/scummvm/scummvm/commit/01000a967ed2bd835ba892cbf2c7964919aeaae2
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: WBASE: Handle enhancement state better
Give BaseGame sole ownership of the active panel, held actions, hover
state, timing reset, and chooser arming. Reset that state and autoplay
on every run and scoped exit, then drain entry input until tracked
controls and the left mouse button are released.
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/hopkins/base.cpp
engines/hopkins/base.h
engines/hopkins/base_enhancements.h
engines/hopkins/base_enhancements_autoplay.cpp
engines/hopkins/base_enhancements_autoplay.h
diff --git a/engines/hopkins/base.cpp b/engines/hopkins/base.cpp
index 5f710122454..8cfff35939c 100644
--- a/engines/hopkins/base.cpp
+++ b/engines/hopkins/base.cpp
@@ -47,6 +47,13 @@ namespace {
static const char *const kBaseKeymapId = "hopkins-base";
static const char *const kDefaultKeymapId = "hopkins-default";
static const char *const kShortcutKeymapId = "game-shortcuts";
+static const uint32 kAutoplayMenuInputDelayMs = 200;
+
+static uint32 enhancementActionMask(uint32 action) {
+ if (action < kActionBaseForward || action > kActionWBASEEnhancementsAutoplay)
+ return 0;
+ return 1U << (action - kActionBaseForward);
+}
// WBASE renders a 320x200 frame through the original Hopkins 640x480
// presentation: 2x pixels with 30-pixel top and bottom borders. Backend mouse
@@ -89,7 +96,7 @@ public:
}
~SessionGuard() {
- _game._input = BaseInputState();
+ _game.resetEnhancementSession();
if (_keymapsReady)
_game.switchKeymaps(false);
if (_audioReady)
@@ -131,7 +138,9 @@ BaseGame::BaseGame(HopkinsEngine *vm) :
_shortcutKeymapWasEnabled(false), _baseKeymapWasEnabled(false), _wbaseEnhancementsKeymapWasEnabled(false),
_inputSuspended(false), _mainMenuRequested(false),
_presentationRefreshRequested(false), _timingResetRequested(false),
- _wbaseEnhancementsNavigationMapVisible(false), _wbaseEnhancementsAutoplayMouseVisible(false),
+ _wbaseEnhancementPanel(kWBASEEnhancementPanelNone), _wbaseEnhancementHeldActions(0),
+ _wbaseAutoplayMenuOpenedAt(0), _wbaseEnhancementInputArmed(false),
+ _wbaseAutoplayMenuInputArmed(false), _wbaseEnhancementsAutoplayMouseVisible(false),
_quitRequested(false) {
_framebuffer.resize(kBaseFrameWidth * kBaseFrameHeight);
Common::fill(_audioLoaded, _audioLoaded + ARRAYSIZE(_audioLoaded), false);
@@ -197,14 +206,11 @@ BaseRunResult BaseGame::run(int entryId) {
if (!session.initialize())
return BaseRunResult(kBaseRunFallback);
- _input = BaseInputState();
+ resetEnhancementSession();
_inputSuspended = false;
_mainMenuRequested = false;
_presentationRefreshRequested = false;
_timingResetRequested = false;
- _wbaseEnhancementsNavigationMapVisible = false;
- _wbaseEnhancementsAutoplay.reset();
- _wbaseEnhancementsAutoplayMouseVisible = false;
_quitRequested = false;
_result = -1;
renderFrame();
@@ -224,18 +230,18 @@ BaseRunResult BaseGame::run(int entryId) {
}
if (_result != -1 || _quitRequested || _vm->shouldQuit())
break;
- if (_inputSuspended || _wbaseEnhancementsNavigationMapVisible || _wbaseEnhancementsAutoplay.menuVisible()) {
+ if (_inputSuspended || enhancementPanelVisible()) {
previousTime = now;
accumulator = 0;
- if (_wbaseEnhancementsAutoplay.menuChoicesVisible(now)) {
+ if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay && _wbaseAutoplayMenuInputArmed) {
const Common::Point mouse = presentationToWBASEPoint(g_system->getEventManager()->getMousePos());
_wbaseEnhancementsAutoplay.updateMenuPointer(mouse.x, mouse.y);
setAutoplayMenuMouseVisible(true);
}
- if ((_wbaseEnhancementsNavigationMapVisible || _wbaseEnhancementsAutoplay.menuVisible()) && !_inputSuspended)
+ if (enhancementPanelVisible() && !_inputSuspended)
renderFrame();
_vm->_soundMan->checkSounds();
- g_system->delayMillis((_wbaseEnhancementsNavigationMapVisible || _wbaseEnhancementsAutoplay.menuVisible()) ? 50 : 10);
+ g_system->delayMillis(enhancementPanelVisible() ? 50 : 10);
continue;
}
const uint32 elapsed = MIN<uint32>(now - previousTime, 250);
@@ -412,6 +418,71 @@ void BaseGame::switchKeymaps(bool entering) {
}
}
+void BaseGame::resetEnhancementSession() {
+ _input = BaseInputState();
+ _wbaseEnhancementPanel = kWBASEEnhancementPanelNone;
+ _wbaseEnhancementHeldActions = 0;
+ _wbaseAutoplayMenuOpenedAt = 0;
+ _wbaseEnhancementInputArmed = !_wbaseEnhancements.enabled();
+ _wbaseAutoplayMenuInputArmed = false;
+ _wbaseEnhancementsAutoplay.reset();
+ setAutoplayMenuMouseVisible(false);
+}
+
+void BaseGame::updateEnhancementActionState(uint32 action, bool pressed) {
+ const uint32 mask = enhancementActionMask(action);
+ if (!mask)
+ return;
+ if (pressed)
+ _wbaseEnhancementHeldActions |= mask;
+ else
+ _wbaseEnhancementHeldActions &= ~mask;
+}
+
+void BaseGame::updateEnhancementInputArming(uint32 now) {
+ if (_inputSuspended)
+ return;
+ const bool mouseReleased = !(g_system->getEventManager()->getButtonState() & Common::EventManager::LBUTTON);
+ if (!_wbaseEnhancementInputArmed && !_wbaseEnhancementHeldActions && mouseReleased) {
+ _wbaseEnhancementInputArmed = true;
+ debug(2, "Hopkins WBASE enhancement input armed after entry boundary");
+ }
+ if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay &&
+ !_wbaseAutoplayMenuInputArmed &&
+ now - _wbaseAutoplayMenuOpenedAt >= kAutoplayMenuInputDelayMs &&
+ !_wbaseEnhancementHeldActions && mouseReleased) {
+ _wbaseAutoplayMenuInputArmed = true;
+ debug(2, "Hopkins WBASE autoplay menu input armed");
+ }
+}
+
+void BaseGame::setEnhancementPanel(WBASEEnhancementPanel panel) {
+ _wbaseEnhancementPanel = panel;
+ _input = BaseInputState();
+ _timingResetRequested = true;
+ _wbaseEnhancementsAutoplay.clearMenuPointer();
+ _wbaseAutoplayMenuInputArmed = false;
+ setAutoplayMenuMouseVisible(false);
+ if (panel == kWBASEEnhancementPanelAutoplay)
+ _wbaseAutoplayMenuOpenedAt = g_system->getMillis();
+ else
+ _wbaseAutoplayMenuOpenedAt = 0;
+ debug(2, "Hopkins WBASE enhancement panel changed to %d", panel);
+ if (_engine && _renderer)
+ renderFrame();
+}
+
+void BaseGame::startSelectedAutoplay() {
+ if (_wbaseEnhancementsAutoplay.startSelected(*_engine))
+ setEnhancementPanel(kWBASEEnhancementPanelNone);
+ else
+ renderFrame();
+}
+
+bool BaseGame::enhancementPanelVisible() const {
+ return _wbaseEnhancementPanel != kWBASEEnhancementPanelNone;
+}
+
void BaseGame::pollInput() {
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
@@ -429,14 +500,23 @@ void BaseGame::pollInput() {
_presentationRefreshRequested = true;
_timingResetRequested = true;
break;
- case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
- handleAction(event.customType, true);
+ case Common::EVENT_CUSTOM_ENGINE_ACTION_START: {
+ updateEnhancementActionState(event.customType, true);
+ if (_wbaseEnhancementInputArmed)
+ handleAction(event.customType, true);
+ else
+ debug(2, "Hopkins WBASE suppressed entry-boundary action %u", event.customType);
break;
+ }
case Common::EVENT_CUSTOM_ENGINE_ACTION_END:
- handleAction(event.customType, false);
+ updateEnhancementActionState(event.customType, false);
+ if (_wbaseEnhancementInputArmed)
+ handleAction(event.customType, false);
break;
case Common::EVENT_FOCUS_LOST:
_input = BaseInputState();
+ _wbaseEnhancementHeldActions = 0;
+ _wbaseAutoplayMenuInputArmed = false;
_inputSuspended = true;
_timingResetRequested = true;
break;
@@ -446,35 +526,31 @@ void BaseGame::pollInput() {
break;
case Common::EVENT_INPUT_CHANGED:
_input = BaseInputState();
+ _wbaseEnhancementHeldActions = 0;
_timingResetRequested = true;
break;
case Common::EVENT_KEYDOWN:
- if (_wbaseEnhancementsAutoplay.menuChoicesVisible(g_system->getMillis()) &&
+ if (_wbaseEnhancementInputArmed && _wbaseAutoplayMenuInputArmed &&
+ _wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay &&
(event.kbd.keycode == Common::KEYCODE_RETURN ||
event.kbd.keycode == Common::KEYCODE_KP_ENTER)) {
- _wbaseEnhancementsAutoplay.startSelected(*_engine);
- setAutoplayMenuMouseVisible(false);
- _input = BaseInputState();
- _timingResetRequested = true;
- renderFrame();
+ startSelectedAutoplay();
}
break;
case Common::EVENT_LBUTTONDOWN:
- if (_wbaseEnhancementsAutoplay.menuChoicesVisible(g_system->getMillis())) {
+ if (_wbaseEnhancementInputArmed && _wbaseAutoplayMenuInputArmed &&
+ _wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay) {
const Common::Point mouse = presentationToWBASEPoint(event.mouse);
if (!_wbaseEnhancementsAutoplay.selectMenuPointer(mouse.x, mouse.y))
break;
- _wbaseEnhancementsAutoplay.startSelected(*_engine);
- setAutoplayMenuMouseVisible(false);
- _input = BaseInputState();
- _timingResetRequested = true;
- renderFrame();
+ startSelectedAutoplay();
}
break;
default:
break;
}
}
+ updateEnhancementInputArming(g_system->getMillis());
if (_mainMenuRequested && _result == -1 && !_quitRequested && !_vm->shouldQuit())
openMainMenu();
@@ -483,8 +559,8 @@ void BaseGame::pollInput() {
}
void BaseGame::handleAction(uint32 action, bool pressed) {
- if (_wbaseEnhancementsAutoplay.menuVisible()) {
- if (!pressed)
+ if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay) {
+ if (!pressed || !_wbaseAutoplayMenuInputArmed)
return;
switch (action) {
case kActionBaseForward:
@@ -495,14 +571,15 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
break;
case kActionBaseFire:
case kActionBaseUse:
- _wbaseEnhancementsAutoplay.startSelected(*_engine);
- setAutoplayMenuMouseVisible(false);
- break;
+ startSelectedAutoplay();
+ return;
case kActionBaseMenu:
case kActionWBASEEnhancementsAutoplay:
- _wbaseEnhancementsAutoplay.closeMenu();
- setAutoplayMenuMouseVisible(false);
- break;
+ setEnhancementPanel(kWBASEEnhancementPanelNone);
+ return;
+ case kActionWBASEEnhancementsNavigationMap:
+ setEnhancementPanel(kWBASEEnhancementPanelNavigationMap);
+ return;
default:
return;
}
@@ -511,6 +588,22 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
renderFrame();
return;
}
+ if (_wbaseEnhancementPanel == kWBASEEnhancementPanelNavigationMap) {
+ if (!pressed)
+ return;
+ switch (action) {
+ case kActionBaseMenu:
+ case kActionWBASEEnhancementsNavigationMap:
+ setEnhancementPanel(kWBASEEnhancementPanelNone);
+ break;
+ case kActionWBASEEnhancementsAutoplay:
+ setEnhancementPanel(kWBASEEnhancementPanelAutoplay);
+ break;
+ default:
+ break;
+ }
+ return;
+ }
if (pressed && _wbaseEnhancementsAutoplay.active()) {
switch (action) {
@@ -553,34 +646,16 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
_input.toggleTextures = true;
break;
case kActionBaseMenu:
- if (pressed) {
- if (_wbaseEnhancementsNavigationMapVisible) {
- _wbaseEnhancementsNavigationMapVisible = false;
- _input = BaseInputState();
- _timingResetRequested = true;
- renderFrame();
- } else {
- _mainMenuRequested = true;
- }
- }
+ if (pressed)
+ _mainMenuRequested = true;
break;
case kActionWBASEEnhancementsNavigationMap:
- if (pressed && _wbaseEnhancements.navigationMapEnabled()) {
- _wbaseEnhancementsNavigationMapVisible = !_wbaseEnhancementsNavigationMapVisible;
- _input = BaseInputState();
- _timingResetRequested = true;
- renderFrame();
- }
+ if (pressed && _wbaseEnhancements.navigationMapEnabled())
+ setEnhancementPanel(kWBASEEnhancementPanelNavigationMap);
break;
case kActionWBASEEnhancementsAutoplay:
- if (pressed && _wbaseEnhancements.enabled()) {
- _wbaseEnhancementsNavigationMapVisible = false;
- setAutoplayMenuMouseVisible(false);
- _wbaseEnhancementsAutoplay.openMenu(g_system->getMillis());
- _input = BaseInputState();
- _timingResetRequested = true;
- renderFrame();
- }
+ if (pressed && _wbaseEnhancements.enabled())
+ setEnhancementPanel(kWBASEEnhancementPanelAutoplay);
break;
default:
break;
@@ -638,12 +713,14 @@ void BaseGame::processSoundEvents() {
void BaseGame::renderFrame() {
if (!_renderer || !_engine || _framebuffer.empty())
return;
- if (_wbaseEnhancementsNavigationMapVisible)
+ _renderer->render(*_engine, _framebuffer.begin());
+ if (_wbaseEnhancementPanel == kWBASEEnhancementPanelNavigationMap) {
_wbaseEnhancements.renderNavigationMap(_data, *_engine, _framebuffer.begin());
- else
- _renderer->render(*_engine, _framebuffer.begin());
- _wbaseEnhancementsAutoplay.render(_data, _engine->entryReturnId(), _framebuffer.begin(), g_system->getMillis(),
- !_wbaseEnhancementsNavigationMapVisible);
+ } else if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay) {
+ _wbaseEnhancementsAutoplay.renderMenu(_data, _engine->entryReturnId(), _framebuffer.begin());
+ } else {
+ _wbaseEnhancementsAutoplay.renderStatus(_data, _framebuffer.begin());
+ }
Common::copy(_framebuffer.begin(), _framebuffer.end(), _vm->_graphicsMan->_frontBuffer);
_vm->_graphicsMan->addDirtyRect(0, 0, kBaseFrameWidth, kBaseFrameHeight);
_vm->_graphicsMan->updateScreen();
diff --git a/engines/hopkins/base.h b/engines/hopkins/base.h
index 3b29ff8b88a..b0af07050a3 100644
--- a/engines/hopkins/base.h
+++ b/engines/hopkins/base.h
@@ -88,6 +88,12 @@ private:
void initializeAudio();
void releaseAudio();
void switchKeymaps(bool entering);
+ void resetEnhancementSession();
+ void updateEnhancementActionState(uint32 action, bool pressed);
+ void updateEnhancementInputArming(uint32 now);
+ void setEnhancementPanel(WBASEEnhancementPanel panel);
+ void startSelectedAutoplay();
+ bool enhancementPanelVisible() const;
void pollInput();
void handleAction(uint32 action, bool pressed);
void openMainMenu();
@@ -117,7 +123,11 @@ private:
bool _mainMenuRequested;
bool _presentationRefreshRequested;
bool _timingResetRequested;
- bool _wbaseEnhancementsNavigationMapVisible;
+ WBASEEnhancementPanel _wbaseEnhancementPanel;
+ uint32 _wbaseEnhancementHeldActions;
+ uint32 _wbaseAutoplayMenuOpenedAt;
+ bool _wbaseEnhancementInputArmed;
+ bool _wbaseAutoplayMenuInputArmed;
bool _wbaseEnhancementsAutoplayMouseVisible;
bool _quitRequested;
};
diff --git a/engines/hopkins/base_enhancements.h b/engines/hopkins/base_enhancements.h
index 67ba2ae83a0..9ca5e6bc7db 100644
--- a/engines/hopkins/base_enhancements.h
+++ b/engines/hopkins/base_enhancements.h
@@ -33,13 +33,18 @@ class BaseEngine;
static const char *const kWBASEEnhancementsConfigKey = "wbase_enhancements";
static const char *const kWBASEEnhancementsKeymapId = "hopkins-wbase-enhancements";
+enum WBASEEnhancementPanel {
+ kWBASEEnhancementPanelNone,
+ kWBASEEnhancementPanelNavigationMap,
+ kWBASEEnhancementPanelAutoplay
+};
+
class WBASEEnhancements {
public:
explicit WBASEEnhancements(const Common::String &targetName);
bool enabled() const { return _enabled; }
bool navigationMapEnabled() const { return _enabled; }
- /** Render the readable opaque navigation-map presentation. */
void renderNavigationMap(const BaseData &data, const BaseEngine &engine, byte *framebuffer) const;
private:
diff --git a/engines/hopkins/base_enhancements_autoplay.cpp b/engines/hopkins/base_enhancements_autoplay.cpp
index 1d360fb48c9..63c06c878b0 100644
--- a/engines/hopkins/base_enhancements_autoplay.cpp
+++ b/engines/hopkins/base_enhancements_autoplay.cpp
@@ -74,7 +74,6 @@ static const int kAutoplayMaximumTicks = 100 * 24;
static const int kAutoplayRecoveryReverseTicks = 6;
static const int kAutoplayRecoveryTurnTicks = 12;
static const int kAutoplayRecoveryAdvanceTicks = 8;
-static const uint32 kAutoplayMenuDelayMs = 1000;
static const int kAutoplayMenuLeft = 42;
static const int kAutoplayMenuTop = 25;
static const int kAutoplayMenuRight = 277;
@@ -232,8 +231,6 @@ WBASEEnhancementsAutoplay::WBASEEnhancementsAutoplay() {
void WBASEEnhancementsAutoplay::reset() {
_active = false;
- _menuVisible = false;
- _menuOpenedAt = 0;
_selectedDestination = 0;
_hoveredDestination = -1;
_destination = -1;
@@ -253,18 +250,7 @@ void WBASEEnhancementsAutoplay::reset() {
_recoveryTurnRight = true;
}
-bool WBASEEnhancementsAutoplay::menuChoicesVisible(uint32 now) const {
- return _menuVisible && now - _menuOpenedAt >= kAutoplayMenuDelayMs;
-}
-
-void WBASEEnhancementsAutoplay::openMenu(uint32 now) {
- _menuVisible = true;
- _menuOpenedAt = now;
- _hoveredDestination = -1;
-}
-
-void WBASEEnhancementsAutoplay::closeMenu() {
- _menuVisible = false;
+void WBASEEnhancementsAutoplay::clearMenuPointer() {
_hoveredDestination = -1;
}
@@ -295,10 +281,18 @@ bool WBASEEnhancementsAutoplay::selectMenuPointer(int x, int y) {
return _hoveredDestination >= 0;
}
-void WBASEEnhancementsAutoplay::startSelected(const BaseEngine &engine) {
+bool WBASEEnhancementsAutoplay::startSelected(const BaseEngine &engine) {
+ return startDestination(engine, _selectedDestination);
+}
+
+bool WBASEEnhancementsAutoplay::startDestination(const BaseEngine &engine, int destination) {
+ if (destination < 0 || destination >= kAutoplayDestinationCount) {
+ cancel();
+ return false;
+ }
+ _selectedDestination = destination;
_destination = _selectedDestination;
_active = true;
- _menuVisible = false;
_lastPlayerX = engine.playerX();
_lastPlayerY = engine.playerY();
_stuckTicks = 0;
@@ -314,15 +308,16 @@ void WBASEEnhancementsAutoplay::startSelected(const BaseEngine &engine) {
warning("Hopkins WBASE enhancement autoplay: no route to room %d",
kAutoplayDestinations[_destination].roomId);
cancel();
+ return false;
} else {
debug(1, "Hopkins WBASE enhancement autoplay: room %d, route %u cells",
kAutoplayDestinations[_destination].roomId, _route.size());
}
+ return true;
}
void WBASEEnhancementsAutoplay::cancel() {
_active = false;
- _menuVisible = false;
_hoveredDestination = -1;
_destination = -1;
_route.clear();
@@ -509,7 +504,7 @@ int WBASEEnhancementsAutoplay::update(const BaseEngine &engine, BaseInputState &
input = BaseInputState();
_routeBlocker = 0;
_movementBlocker = 0;
- if (!_active || _menuVisible || _destination < 0 || _destination >= kAutoplayDestinationCount)
+ if (!_active || _destination < 0 || _destination >= kAutoplayDestinationCount)
return -1;
// Complete after 100 active seconds so forced autoplay cannot remain stuck.
@@ -636,9 +631,9 @@ int WBASEEnhancementsAutoplay::update(const BaseEngine &engine, BaseInputState &
return -1;
}
-void WBASEEnhancementsAutoplay::render(const BaseData &data, int returnRoomId,
- byte *framebuffer, uint32 now, bool showStatus) const {
- if (!framebuffer || (!_menuVisible && (!_active || !showStatus)))
+void WBASEEnhancementsAutoplay::renderMenu(const BaseData &data, int returnRoomId,
+ byte *framebuffer) const {
+ if (!framebuffer)
return;
Graphics::Surface surface;
@@ -654,35 +649,49 @@ void WBASEEnhancementsAutoplay::render(const BaseData &data, int returnRoomId,
const byte text = nearestPaletteColor(palette, 255, 255, 255);
const byte selected = nearestPaletteColor(palette, 255, 220, 0);
- if (_menuVisible) {
- if (!menuChoicesVisible(now))
- return;
- fillRect(framebuffer, kAutoplayMenuLeft, kAutoplayMenuTop,
- kAutoplayMenuRight, kAutoplayMenuBottom, background);
- drawFrame(framebuffer, kAutoplayMenuLeft, kAutoplayMenuTop,
- kAutoplayMenuRight, kAutoplayMenuBottom, border);
- font->drawString(&surface, _("WBASE AUTOPLAY"), kAutoplayMenuLeft + 4,
- kAutoplayMenuTop + 7, kAutoplayMenuRight - kAutoplayMenuLeft - 7,
- text, Graphics::kTextAlignCenter);
- font->drawString(&surface, _("Fight your way to:"), kAutoplayMenuLeft + 4,
- kAutoplayMenuTop + 21, kAutoplayMenuRight - kAutoplayMenuLeft - 7,
- text, Graphics::kTextAlignCenter);
- for (int index = 0; index < kAutoplayDestinationCount; ++index) {
- const Common::String label = menuDestinationLabel(index, returnRoomId);
- const bool highlighted = index == (_hoveredDestination >= 0 ? _hoveredDestination : _selectedDestination);
- font->drawString(&surface, Common::String::format("%c %s", highlighted ? '>' : ' ', label.c_str()),
- kAutoplayMenuLeft + 14, kAutoplayMenuChoiceTop + index * kAutoplayMenuChoiceSpacing,
- kAutoplayMenuRight - kAutoplayMenuLeft - 27, highlighted ? selected : text);
- }
- font->drawString(&surface, _("Up/Down: choose"), kAutoplayMenuLeft + 8,
- kAutoplayMenuBottom - 25, 104, text);
- font->drawString(&surface, _("Enter/Space: start"), kAutoplayMenuLeft + 116,
- kAutoplayMenuBottom - 25, 111, text);
- font->drawString(&surface, _("Esc/A: cancel"), kAutoplayMenuLeft + 4,
- kAutoplayMenuBottom - 13, kAutoplayMenuRight - kAutoplayMenuLeft - 7,
- text, Graphics::kTextAlignCenter);
- return;
+ fillRect(framebuffer, kAutoplayMenuLeft, kAutoplayMenuTop,
+ kAutoplayMenuRight, kAutoplayMenuBottom, background);
+ drawFrame(framebuffer, kAutoplayMenuLeft, kAutoplayMenuTop,
+ kAutoplayMenuRight, kAutoplayMenuBottom, border);
+ font->drawString(&surface, _("WBASE AUTOPLAY"), kAutoplayMenuLeft + 4,
+ kAutoplayMenuTop + 7, kAutoplayMenuRight - kAutoplayMenuLeft - 7,
+ text, Graphics::kTextAlignCenter);
+ font->drawString(&surface, _("Fight your way to:"), kAutoplayMenuLeft + 4,
+ kAutoplayMenuTop + 21, kAutoplayMenuRight - kAutoplayMenuLeft - 7,
+ text, Graphics::kTextAlignCenter);
+ for (int index = 0; index < kAutoplayDestinationCount; ++index) {
+ const Common::String label = menuDestinationLabel(index, returnRoomId);
+ const bool highlighted = index ==
+ (_hoveredDestination >= 0 ? _hoveredDestination : _selectedDestination);
+ font->drawString(&surface, Common::String::format("%c %s", highlighted ? '>' : ' ', label.c_str()),
+ kAutoplayMenuLeft + 14, kAutoplayMenuChoiceTop + index * kAutoplayMenuChoiceSpacing,
+ kAutoplayMenuRight - kAutoplayMenuLeft - 27,
+ highlighted ? selected : text);
}
+ font->drawString(&surface, _("Up/Down: choose"), kAutoplayMenuLeft + 8,
+ kAutoplayMenuBottom - 25, 104, text);
+ font->drawString(&surface, _("Enter/Space: start"), kAutoplayMenuLeft + 116,
+ kAutoplayMenuBottom - 25, 111, text);
+ font->drawString(&surface, _("Esc/A: cancel"), kAutoplayMenuLeft + 4,
+ kAutoplayMenuBottom - 13, kAutoplayMenuRight - kAutoplayMenuLeft - 7,
+ text, Graphics::kTextAlignCenter);
+}
+
+void WBASEEnhancementsAutoplay::renderStatus(const BaseData &data, byte *framebuffer) const {
+ if (!framebuffer || !_active)
+ return;
+
+ Graphics::Surface surface;
+ surface.init(kBaseFrameWidth, kBaseFrameHeight, kBaseFrameWidth, framebuffer,
+ Graphics::PixelFormat::createFormatCLUT8());
+ const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
+ if (!font)
+ return;
+
+ const byte *palette = data.palette();
+ const byte background = nearestPaletteColor(palette, 0, 0, 0);
+ const byte border = nearestPaletteColor(palette, 190, 190, 190);
+ const byte text = nearestPaletteColor(palette, 255, 255, 255);
const int secondsRemaining = MAX(0, (kAutoplayMaximumTicks - _activeTicks + 23) / 24);
const Common::String status = Common::String::format("To %s - %ds",
diff --git a/engines/hopkins/base_enhancements_autoplay.h b/engines/hopkins/base_enhancements_autoplay.h
index 3f9b8cd6198..6f10d03cd6b 100644
--- a/engines/hopkins/base_enhancements_autoplay.h
+++ b/engines/hopkins/base_enhancements_autoplay.h
@@ -38,22 +38,20 @@ public:
void reset();
bool active() const { return _active; }
- bool menuVisible() const { return _menuVisible; }
- bool menuChoicesVisible(uint32 now) const;
- void openMenu(uint32 now);
- void closeMenu();
+ void clearMenuPointer();
void selectPrevious();
void selectNext();
void updateMenuPointer(int x, int y);
bool selectMenuPointer(int x, int y);
- void startSelected(const BaseEngine &engine);
+ bool startSelected(const BaseEngine &engine);
+ bool startDestination(const BaseEngine &engine, int destination);
void cancel();
/** Return the forced destination room, or -1, after updating input. */
int update(const BaseEngine &engine, BaseInputState &input);
- void render(const BaseData &data, int returnRoomId,
- byte *framebuffer, uint32 now, bool showStatus) const;
+ void renderMenu(const BaseData &data, int returnRoomId, byte *framebuffer) const;
+ void renderStatus(const BaseData &data, byte *framebuffer) const;
static int destinationCount();
static int destinationRoomId(int index);
@@ -71,8 +69,6 @@ private:
void updateRecovery(const BaseEngine &engine, BaseInputState &input);
bool _active;
- bool _menuVisible;
- uint32 _menuOpenedAt;
int _selectedDestination;
int _hoveredDestination;
int _destination;
Commit: 57cc316938b781541cb2e690e6adea721bd60337
https://github.com/scummvm/scummvm/commit/57cc316938b781541cb2e690e6adea721bd60337
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: Expose forced WBASE autoplay
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/hopkins/base_enhancements.cpp
engines/hopkins/base_enhancements.h
engines/hopkins/detection.h
engines/hopkins/detection_tables.h
engines/hopkins/metaengine.cpp
diff --git a/engines/hopkins/base_enhancements.cpp b/engines/hopkins/base_enhancements.cpp
index b97ab776f9d..024da232ebf 100644
--- a/engines/hopkins/base_enhancements.cpp
+++ b/engines/hopkins/base_enhancements.cpp
@@ -285,7 +285,8 @@ static void renderNavigationMapOpaque(const BaseData &data, const BaseEngine &en
} // End of anonymous namespace
WBASEEnhancements::WBASEEnhancements(const Common::String &targetName) :
- _enabled(ConfMan.getBool(kWBASEEnhancementsConfigKey, targetName)) {
+ _enabled(ConfMan.getBool(kWBASEEnhancementsConfigKey, targetName)),
+ _forcedAutoplay(ConfMan.getBool(kWBASEForcedAutoplayConfigKey, targetName)) {
}
void WBASEEnhancements::renderNavigationMap(const BaseData &data, const BaseEngine &engine, byte *framebuffer) const {
diff --git a/engines/hopkins/base_enhancements.h b/engines/hopkins/base_enhancements.h
index 9ca5e6bc7db..81b13059a1b 100644
--- a/engines/hopkins/base_enhancements.h
+++ b/engines/hopkins/base_enhancements.h
@@ -31,6 +31,7 @@ class BaseData;
class BaseEngine;
static const char *const kWBASEEnhancementsConfigKey = "wbase_enhancements";
+static const char *const kWBASEForcedAutoplayConfigKey = "wbase_forced_autoplay";
static const char *const kWBASEEnhancementsKeymapId = "hopkins-wbase-enhancements";
enum WBASEEnhancementPanel {
@@ -44,11 +45,14 @@ public:
explicit WBASEEnhancements(const Common::String &targetName);
bool enabled() const { return _enabled; }
- bool navigationMapEnabled() const { return _enabled; }
+ bool forcedAutoplayEnabled() const { return _forcedAutoplay; }
+ bool controlsEnabled() const { return _enabled || _forcedAutoplay; }
+ bool navigationMapEnabled() const { return controlsEnabled(); }
void renderNavigationMap(const BaseData &data, const BaseEngine &engine, byte *framebuffer) const;
private:
bool _enabled;
+ bool _forcedAutoplay;
};
} // End of namespace Hopkins
diff --git a/engines/hopkins/detection.h b/engines/hopkins/detection.h
index ae76a30bf92..82fce0eec0d 100644
--- a/engines/hopkins/detection.h
+++ b/engines/hopkins/detection.h
@@ -33,6 +33,7 @@ struct HopkinsGameDescription {
#define GAMEOPTION_GORE_DEFAULT_ON GUIO_GAMEOPTIONS1
#define GAMEOPTION_GORE_DEFAULT_OFF GUIO_GAMEOPTIONS2
#define GAMEOPTION_WBASE_ENHANCEMENTS GUIO_GAMEOPTIONS3
+#define GAMEOPTION_WBASE_FORCED_AUTOPLAY GUIO_GAMEOPTIONS4
} // End of namespace Hopkins
diff --git a/engines/hopkins/detection_tables.h b/engines/hopkins/detection_tables.h
index 99100789e6a..9ac23d3dd56 100644
--- a/engines/hopkins/detection_tables.h
+++ b/engines/hopkins/detection_tables.h
@@ -68,7 +68,7 @@ static const HopkinsGameDescription gameDescriptions[] = {
Common::ES_ESP,
Common::kPlatformWindows,
ADGF_NO_FLAGS,
- GUIO3(GAMEOPTION_GORE_DEFAULT_ON, GAMEOPTION_WBASE_ENHANCEMENTS, GUIO_NOMIDI)
+ GUIO4(GAMEOPTION_GORE_DEFAULT_ON, GAMEOPTION_WBASE_ENHANCEMENTS, GAMEOPTION_WBASE_FORCED_AUTOPLAY, GUIO_NOMIDI)
},
},
{
@@ -80,7 +80,7 @@ static const HopkinsGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformWindows,
ADGF_NO_FLAGS,
- GUIO3(GAMEOPTION_GORE_DEFAULT_OFF, GAMEOPTION_WBASE_ENHANCEMENTS, GUIO_NOMIDI)
+ GUIO4(GAMEOPTION_GORE_DEFAULT_OFF, GAMEOPTION_WBASE_ENHANCEMENTS, GAMEOPTION_WBASE_FORCED_AUTOPLAY, GUIO_NOMIDI)
},
},
{
@@ -92,7 +92,7 @@ static const HopkinsGameDescription gameDescriptions[] = {
Common::RU_RUS,
Common::kPlatformWindows,
ADGF_NO_FLAGS,
- GUIO3(GAMEOPTION_GORE_DEFAULT_ON, GAMEOPTION_WBASE_ENHANCEMENTS, GUIO_NOMIDI)
+ GUIO4(GAMEOPTION_GORE_DEFAULT_ON, GAMEOPTION_WBASE_ENHANCEMENTS, GAMEOPTION_WBASE_FORCED_AUTOPLAY, GUIO_NOMIDI)
},
},
{
@@ -128,7 +128,7 @@ static const HopkinsGameDescription gameDescriptions[] = {
Common::FR_FRA,
Common::kPlatformWindows,
ADGF_NO_FLAGS,
- GUIO3(GAMEOPTION_GORE_DEFAULT_ON, GAMEOPTION_WBASE_ENHANCEMENTS, GUIO_NOMIDI)
+ GUIO4(GAMEOPTION_GORE_DEFAULT_ON, GAMEOPTION_WBASE_ENHANCEMENTS, GAMEOPTION_WBASE_FORCED_AUTOPLAY, GUIO_NOMIDI)
},
},
@@ -141,7 +141,7 @@ static const HopkinsGameDescription gameDescriptions[] = {
Common::PL_POL,
Common::kPlatformWindows,
ADGF_NO_FLAGS,
- GUIO3(GAMEOPTION_GORE_DEFAULT_OFF, GAMEOPTION_WBASE_ENHANCEMENTS, GUIO_NOMIDI)
+ GUIO4(GAMEOPTION_GORE_DEFAULT_OFF, GAMEOPTION_WBASE_ENHANCEMENTS, GAMEOPTION_WBASE_FORCED_AUTOPLAY, GUIO_NOMIDI)
},
},
diff --git a/engines/hopkins/metaengine.cpp b/engines/hopkins/metaengine.cpp
index 3e7bcb0fb1a..d4d1e455b79 100644
--- a/engines/hopkins/metaengine.cpp
+++ b/engines/hopkins/metaengine.cpp
@@ -77,7 +77,17 @@ static const ADExtraGuiOptionsMap optionsList[] = {
0
}
},
-
+ {
+ GAMEOPTION_WBASE_FORCED_AUTOPLAY,
+ {
+ _s("WBASE Forced Autoplay"),
+ _s("Require an autoplay destination in Windows base shooter"),
+ kWBASEForcedAutoplayConfigKey,
+ false,
+ 0,
+ 0
+ }
+ },
AD_EXTRA_GUI_OPTIONS_TERMINATOR
};
Commit: 81232090e5286447d012c12cf5162e25aaf0067d
https://github.com/scummvm/scummvm/commit/81232090e5286447d012c12cf5162e25aaf0067d
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: WBASE: Add forced autoplay mode
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/hopkins/base.cpp
engines/hopkins/base.h
engines/hopkins/base_enhancements_autoplay.cpp
engines/hopkins/base_enhancements_autoplay.h
diff --git a/engines/hopkins/base.cpp b/engines/hopkins/base.cpp
index 8cfff35939c..bafc4ee0d84 100644
--- a/engines/hopkins/base.cpp
+++ b/engines/hopkins/base.cpp
@@ -48,6 +48,7 @@ static const char *const kBaseKeymapId = "hopkins-base";
static const char *const kDefaultKeymapId = "hopkins-default";
static const char *const kShortcutKeymapId = "game-shortcuts";
static const uint32 kAutoplayMenuInputDelayMs = 200;
+static const int kForcedAutoplayPromptTicks = 12;
static uint32 enhancementActionMask(uint32 action) {
if (action < kActionBaseForward || action > kActionWBASEEnhancementsAutoplay)
@@ -139,7 +140,8 @@ BaseGame::BaseGame(HopkinsEngine *vm) :
_inputSuspended(false), _mainMenuRequested(false),
_presentationRefreshRequested(false), _timingResetRequested(false),
_wbaseEnhancementPanel(kWBASEEnhancementPanelNone), _wbaseEnhancementHeldActions(0),
- _wbaseAutoplayMenuOpenedAt(0), _wbaseEnhancementInputArmed(false),
+ _wbaseAutoplayMenuOpenedAt(0), _wbaseForcedAutoplayPromptTicks(0),
+ _wbaseForcedAutoplayPromptIssued(false), _wbaseEnhancementInputArmed(false),
_wbaseAutoplayMenuInputArmed(false), _wbaseEnhancementsAutoplayMouseVisible(false),
_quitRequested(false) {
_framebuffer.resize(kBaseFrameWidth * kBaseFrameHeight);
@@ -259,6 +261,13 @@ BaseRunResult BaseGame::run(int entryId) {
_result = forcedRoom;
break;
}
+ if (_wbaseEnhancements.forcedAutoplayEnabled() && !_wbaseEnhancementsAutoplay.active()) {
+ openAutoplayMenu();
+ accumulator = 0;
+ break;
+ }
+ tickInput = &autoplayInput;
+ } else if (_wbaseEnhancements.forcedAutoplayEnabled()) {
tickInput = &autoplayInput;
}
const int tickResult = _engine->tick(*tickInput);
@@ -266,9 +275,17 @@ BaseRunResult BaseGame::run(int entryId) {
if (tickResult >= 0)
_result = tickResult;
advanced = true;
+ if (_result == -1 && _wbaseEnhancements.forcedAutoplayEnabled() &&
+ !_wbaseForcedAutoplayPromptIssued && _wbaseForcedAutoplayPromptTicks > 0 &&
+ --_wbaseForcedAutoplayPromptTicks == 0) {
+ _wbaseForcedAutoplayPromptIssued = true;
+ openAutoplayMenu();
+ accumulator = 0;
+ break;
+ }
}
- if (advanced && _result == -1)
+ if (advanced && _result == -1 && !enhancementPanelVisible())
renderFrame();
_vm->_soundMan->checkSounds();
g_system->delayMillis(2);
@@ -403,7 +420,7 @@ void BaseGame::switchKeymaps(bool entering) {
if (baseKeymap)
baseKeymap->setEnabled(true);
if (wbaseEnhancementsKeymap)
- wbaseEnhancementsKeymap->setEnabled(_wbaseEnhancements.enabled());
+ wbaseEnhancementsKeymap->setEnabled(_wbaseEnhancements.controlsEnabled());
_keymapsSwitched = true;
} else if (_keymapsSwitched) {
if (defaultKeymap)
@@ -423,7 +440,9 @@ void BaseGame::resetEnhancementSession() {
_wbaseEnhancementPanel = kWBASEEnhancementPanelNone;
_wbaseEnhancementHeldActions = 0;
_wbaseAutoplayMenuOpenedAt = 0;
- _wbaseEnhancementInputArmed = !_wbaseEnhancements.enabled();
+ _wbaseForcedAutoplayPromptTicks = _wbaseEnhancements.forcedAutoplayEnabled() ? kForcedAutoplayPromptTicks : 0;
+ _wbaseForcedAutoplayPromptIssued = false;
+ _wbaseEnhancementInputArmed = !_wbaseEnhancements.controlsEnabled();
_wbaseAutoplayMenuInputArmed = false;
_wbaseEnhancementsAutoplay.reset();
setAutoplayMenuMouseVisible(false);
@@ -472,11 +491,30 @@ void BaseGame::setEnhancementPanel(WBASEEnhancementPanel panel) {
renderFrame();
}
+void BaseGame::openAutoplayMenu() {
+ if (_wbaseEnhancements.forcedAutoplayEnabled()) {
+ _wbaseForcedAutoplayPromptTicks = 0;
+ _wbaseForcedAutoplayPromptIssued = true;
+ }
+ setEnhancementPanel(kWBASEEnhancementPanelAutoplay);
+}
+
+void BaseGame::closeNavigationMap() {
+ if (_wbaseEnhancements.forcedAutoplayEnabled() &&
+ _wbaseForcedAutoplayPromptIssued && !_wbaseEnhancementsAutoplay.active())
+ setEnhancementPanel(kWBASEEnhancementPanelAutoplay);
+ else
+ setEnhancementPanel(kWBASEEnhancementPanelNone);
+}
+
void BaseGame::startSelectedAutoplay() {
- if (_wbaseEnhancementsAutoplay.startSelected(*_engine))
+ if (_wbaseEnhancementsAutoplay.startSelected(*_engine)) {
+ _wbaseForcedAutoplayPromptTicks = 0;
+ _wbaseForcedAutoplayPromptIssued = true;
setEnhancementPanel(kWBASEEnhancementPanelNone);
- else
+ } else {
renderFrame();
+ }
}
bool BaseGame::enhancementPanelVisible() const {
@@ -560,7 +598,13 @@ void BaseGame::pollInput() {
void BaseGame::handleAction(uint32 action, bool pressed) {
if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay) {
- if (!pressed || !_wbaseAutoplayMenuInputArmed)
+ if (!pressed)
+ return;
+ if (_wbaseEnhancements.forcedAutoplayEnabled() && action == kActionBaseMenu) {
+ _mainMenuRequested = true;
+ return;
+ }
+ if (!_wbaseAutoplayMenuInputArmed)
return;
switch (action) {
case kActionBaseForward:
@@ -574,8 +618,14 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
startSelectedAutoplay();
return;
case kActionBaseMenu:
+ if (_wbaseEnhancements.forcedAutoplayEnabled())
+ _mainMenuRequested = true;
+ else
+ setEnhancementPanel(kWBASEEnhancementPanelNone);
+ return;
case kActionWBASEEnhancementsAutoplay:
- setEnhancementPanel(kWBASEEnhancementPanelNone);
+ if (!_wbaseEnhancements.forcedAutoplayEnabled() || _wbaseEnhancementsAutoplay.active())
+ setEnhancementPanel(kWBASEEnhancementPanelNone);
return;
case kActionWBASEEnhancementsNavigationMap:
setEnhancementPanel(kWBASEEnhancementPanelNavigationMap);
@@ -592,12 +642,35 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
if (!pressed)
return;
switch (action) {
+ case kActionWBASEEnhancementsNavigationMap:
+ closeNavigationMap();
+ break;
case kActionBaseMenu:
+ if (_wbaseEnhancements.forcedAutoplayEnabled())
+ _mainMenuRequested = true;
+ else
+ setEnhancementPanel(kWBASEEnhancementPanelNone);
+ break;
+ case kActionWBASEEnhancementsAutoplay:
+ openAutoplayMenu();
+ break;
+ default:
+ break;
+ }
+ return;
+ }
+ if (_wbaseEnhancements.forcedAutoplayEnabled()) {
+ if (!pressed)
+ return;
+ switch (action) {
+ case kActionBaseMenu:
+ _mainMenuRequested = true;
+ break;
case kActionWBASEEnhancementsNavigationMap:
- setEnhancementPanel(kWBASEEnhancementPanelNone);
+ setEnhancementPanel(kWBASEEnhancementPanelNavigationMap);
break;
case kActionWBASEEnhancementsAutoplay:
- setEnhancementPanel(kWBASEEnhancementPanelAutoplay);
+ openAutoplayMenu();
break;
default:
break;
@@ -655,7 +728,7 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
break;
case kActionWBASEEnhancementsAutoplay:
if (pressed && _wbaseEnhancements.enabled())
- setEnhancementPanel(kWBASEEnhancementPanelAutoplay);
+ openAutoplayMenu();
break;
default:
break;
@@ -717,9 +790,11 @@ void BaseGame::renderFrame() {
if (_wbaseEnhancementPanel == kWBASEEnhancementPanelNavigationMap) {
_wbaseEnhancements.renderNavigationMap(_data, *_engine, _framebuffer.begin());
} else if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay) {
- _wbaseEnhancementsAutoplay.renderMenu(_data, _engine->entryReturnId(), _framebuffer.begin());
+ _wbaseEnhancementsAutoplay.renderMenu(_data, _engine->entryReturnId(), _framebuffer.begin(),
+ _wbaseEnhancements.forcedAutoplayEnabled());
} else {
- _wbaseEnhancementsAutoplay.renderStatus(_data, _framebuffer.begin());
+ _wbaseEnhancementsAutoplay.renderStatus(_data, _framebuffer.begin(),
+ _wbaseEnhancements.forcedAutoplayEnabled());
}
Common::copy(_framebuffer.begin(), _framebuffer.end(), _vm->_graphicsMan->_frontBuffer);
_vm->_graphicsMan->addDirtyRect(0, 0, kBaseFrameWidth, kBaseFrameHeight);
diff --git a/engines/hopkins/base.h b/engines/hopkins/base.h
index b0af07050a3..f4e3668b805 100644
--- a/engines/hopkins/base.h
+++ b/engines/hopkins/base.h
@@ -92,6 +92,8 @@ private:
void updateEnhancementActionState(uint32 action, bool pressed);
void updateEnhancementInputArming(uint32 now);
void setEnhancementPanel(WBASEEnhancementPanel panel);
+ void openAutoplayMenu();
+ void closeNavigationMap();
void startSelectedAutoplay();
bool enhancementPanelVisible() const;
void pollInput();
@@ -126,6 +128,8 @@ private:
WBASEEnhancementPanel _wbaseEnhancementPanel;
uint32 _wbaseEnhancementHeldActions;
uint32 _wbaseAutoplayMenuOpenedAt;
+ int _wbaseForcedAutoplayPromptTicks;
+ bool _wbaseForcedAutoplayPromptIssued;
bool _wbaseEnhancementInputArmed;
bool _wbaseAutoplayMenuInputArmed;
bool _wbaseEnhancementsAutoplayMouseVisible;
diff --git a/engines/hopkins/base_enhancements_autoplay.cpp b/engines/hopkins/base_enhancements_autoplay.cpp
index 63c06c878b0..7c6ff6894c5 100644
--- a/engines/hopkins/base_enhancements_autoplay.cpp
+++ b/engines/hopkins/base_enhancements_autoplay.cpp
@@ -632,7 +632,7 @@ int WBASEEnhancementsAutoplay::update(const BaseEngine &engine, BaseInputState &
}
void WBASEEnhancementsAutoplay::renderMenu(const BaseData &data, int returnRoomId,
- byte *framebuffer) const {
+ byte *framebuffer, bool forcedMode) const {
if (!framebuffer)
return;
@@ -672,12 +672,15 @@ void WBASEEnhancementsAutoplay::renderMenu(const BaseData &data, int returnRoomI
kAutoplayMenuBottom - 25, 104, text);
font->drawString(&surface, _("Enter/Space: start"), kAutoplayMenuLeft + 116,
kAutoplayMenuBottom - 25, 111, text);
- font->drawString(&surface, _("Esc/A: cancel"), kAutoplayMenuLeft + 4,
+ const Common::U32String footer = forcedMode ?
+ (_active ? _("A: resume / M: map") : _("Select a destination")) :
+ _("Esc/A: cancel");
+ font->drawString(&surface, footer, kAutoplayMenuLeft + 4,
kAutoplayMenuBottom - 13, kAutoplayMenuRight - kAutoplayMenuLeft - 7,
text, Graphics::kTextAlignCenter);
}
-void WBASEEnhancementsAutoplay::renderStatus(const BaseData &data, byte *framebuffer) const {
+void WBASEEnhancementsAutoplay::renderStatus(const BaseData &data, byte *framebuffer, bool forcedMode) const {
if (!framebuffer || !_active)
return;
@@ -694,7 +697,7 @@ void WBASEEnhancementsAutoplay::renderStatus(const BaseData &data, byte *framebu
const byte text = nearestPaletteColor(palette, 255, 255, 255);
const int secondsRemaining = MAX(0, (kAutoplayMaximumTicks - _activeTicks + 23) / 24);
- const Common::String status = Common::String::format("To %s - %ds",
+ const Common::String status = Common::String::format(forcedMode ? "LOCKED AUTO: %s - %ds" : "AUTO: %s - %ds",
kAutoplayDestinations[_destination].label, secondsRemaining);
const int statusWidth = MIN(font->getStringWidth(status) + 10, kBaseFrameWidth - 8);
const int statusLeft = (kBaseFrameWidth - statusWidth) / 2;
diff --git a/engines/hopkins/base_enhancements_autoplay.h b/engines/hopkins/base_enhancements_autoplay.h
index 6f10d03cd6b..1ea5d6fbcac 100644
--- a/engines/hopkins/base_enhancements_autoplay.h
+++ b/engines/hopkins/base_enhancements_autoplay.h
@@ -50,8 +50,9 @@ public:
/** Return the forced destination room, or -1, after updating input. */
int update(const BaseEngine &engine, BaseInputState &input);
- void renderMenu(const BaseData &data, int returnRoomId, byte *framebuffer) const;
- void renderStatus(const BaseData &data, byte *framebuffer) const;
+ void renderMenu(const BaseData &data, int returnRoomId, byte *framebuffer,
+ bool forcedMode) const;
+ void renderStatus(const BaseData &data, byte *framebuffer, bool forcedMode) const;
static int destinationCount();
static int destinationRoomId(int index);
Commit: 5c26611e1c0c1d0b6101c0faba81efa6009eab49
https://github.com/scummvm/scummvm/commit/5c26611e1c0c1d0b6101c0faba81efa6009eab49
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: WBASE: Add pointer controls to WBASE enhancements
Add compact M and A controls to the unused lower-right HUD strip when
either WBASE option is active. Use palette-derived colors to show
idle, hover, map, chooser, running, and locked states without changing
the restored world viewport.
Use the existing 640x480-to-320x200 coordinate transform for mouse
and touch input. Push ScummVM's default arrow only for the WBASE
enhancement session and restore the previous cursor stack and
presentation state on exit.
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/hopkins/base.cpp
engines/hopkins/base.h
engines/hopkins/base_enhancements.cpp
engines/hopkins/base_enhancements.h
diff --git a/engines/hopkins/base.cpp b/engines/hopkins/base.cpp
index bafc4ee0d84..2d4aefc1f4e 100644
--- a/engines/hopkins/base.cpp
+++ b/engines/hopkins/base.cpp
@@ -39,6 +39,7 @@
#include "common/system.h"
#include "common/textconsole.h"
#include "common/util.h"
+#include "graphics/cursorman.h"
namespace Hopkins {
@@ -98,6 +99,7 @@ public:
~SessionGuard() {
_game.resetEnhancementSession();
+ _game.releaseEnhancementCursor();
if (_keymapsReady)
_game.switchKeymaps(false);
if (_audioReady)
@@ -115,6 +117,7 @@ public:
if (!_game.initializePresentation(_backup))
return false;
_presentationReady = true;
+ _game.initializeEnhancementCursor();
_game.initializeAudio();
_audioReady = true;
@@ -141,8 +144,9 @@ BaseGame::BaseGame(HopkinsEngine *vm) :
_presentationRefreshRequested(false), _timingResetRequested(false),
_wbaseEnhancementPanel(kWBASEEnhancementPanelNone), _wbaseEnhancementHeldActions(0),
_wbaseAutoplayMenuOpenedAt(0), _wbaseForcedAutoplayPromptTicks(0),
- _wbaseForcedAutoplayPromptIssued(false), _wbaseEnhancementInputArmed(false),
- _wbaseAutoplayMenuInputArmed(false), _wbaseEnhancementsAutoplayMouseVisible(false),
+ _wbaseForcedAutoplayPromptIssued(false), _wbaseEnhancementHoveredControl(kWBASEEnhancementControlNone),
+ _wbaseEnhancementInputArmed(false), _wbaseAutoplayMenuInputArmed(false),
+ _wbaseEnhancementCursorPushed(false),
_quitRequested(false) {
_framebuffer.resize(kBaseFrameWidth * kBaseFrameHeight);
Common::fill(_audioLoaded, _audioLoaded + ARRAYSIZE(_audioLoaded), false);
@@ -238,7 +242,6 @@ BaseRunResult BaseGame::run(int entryId) {
if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay && _wbaseAutoplayMenuInputArmed) {
const Common::Point mouse = presentationToWBASEPoint(g_system->getEventManager()->getMousePos());
_wbaseEnhancementsAutoplay.updateMenuPointer(mouse.x, mouse.y);
- setAutoplayMenuMouseVisible(true);
}
if (enhancementPanelVisible() && !_inputSuspended)
renderFrame();
@@ -435,6 +438,27 @@ void BaseGame::switchKeymaps(bool entering) {
}
}
+void BaseGame::initializeEnhancementCursor() {
+ if (!_wbaseEnhancements.controlsEnabled() || _wbaseEnhancementCursorPushed)
+ return;
+ CursorMan.setDefaultArrowCursor(true);
+ CursorMan.showMouse(true);
+ if (_vm->_events)
+ _vm->_events->_mouseFl = true;
+ _wbaseEnhancementCursorPushed = true;
+}
+
+void BaseGame::releaseEnhancementCursor() {
+ if (!_wbaseEnhancementCursorPushed)
+ return;
+ CursorMan.showMouse(false);
+ CursorMan.popCursor();
+ CursorMan.popCursorPalette();
+ if (_vm->_events)
+ _vm->_events->_mouseFl = false;
+ _wbaseEnhancementCursorPushed = false;
+}
+
void BaseGame::resetEnhancementSession() {
_input = BaseInputState();
_wbaseEnhancementPanel = kWBASEEnhancementPanelNone;
@@ -442,10 +466,10 @@ void BaseGame::resetEnhancementSession() {
_wbaseAutoplayMenuOpenedAt = 0;
_wbaseForcedAutoplayPromptTicks = _wbaseEnhancements.forcedAutoplayEnabled() ? kForcedAutoplayPromptTicks : 0;
_wbaseForcedAutoplayPromptIssued = false;
+ _wbaseEnhancementHoveredControl = kWBASEEnhancementControlNone;
_wbaseEnhancementInputArmed = !_wbaseEnhancements.controlsEnabled();
_wbaseAutoplayMenuInputArmed = false;
_wbaseEnhancementsAutoplay.reset();
- setAutoplayMenuMouseVisible(false);
}
void BaseGame::updateEnhancementActionState(uint32 action, bool pressed) {
@@ -481,7 +505,6 @@ void BaseGame::setEnhancementPanel(WBASEEnhancementPanel panel) {
_timingResetRequested = true;
_wbaseEnhancementsAutoplay.clearMenuPointer();
_wbaseAutoplayMenuInputArmed = false;
- setAutoplayMenuMouseVisible(false);
if (panel == kWBASEEnhancementPanelAutoplay)
_wbaseAutoplayMenuOpenedAt = g_system->getMillis();
else
@@ -517,6 +540,34 @@ void BaseGame::startSelectedAutoplay() {
}
}
+void BaseGame::updateEnhancementPointer(int x, int y) {
+ const WBASEEnhancementControl hoveredControl = _wbaseEnhancements.controlAtPoint(x, y);
+ if (_wbaseEnhancementHoveredControl == hoveredControl)
+ return;
+ _wbaseEnhancementHoveredControl = hoveredControl;
+ if (_engine && _renderer)
+ renderFrame();
+}
+
+bool BaseGame::handleEnhancementControlClick(int x, int y) {
+ const WBASEEnhancementControl control = _wbaseEnhancements.controlAtPoint(x, y);
+ if (control == kWBASEEnhancementControlNone)
+ return false;
+ _wbaseEnhancementHoveredControl = control;
+ if (control == kWBASEEnhancementControlNavigationMap) {
+ if (_wbaseEnhancementPanel == kWBASEEnhancementPanelNavigationMap)
+ closeNavigationMap();
+ else
+ setEnhancementPanel(kWBASEEnhancementPanelNavigationMap);
+ } else if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay) {
+ if (!_wbaseEnhancements.forcedAutoplayEnabled() || _wbaseEnhancementsAutoplay.active())
+ setEnhancementPanel(kWBASEEnhancementPanelNone);
+ } else {
+ openAutoplayMenu();
+ }
+ return true;
+}
+
bool BaseGame::enhancementPanelVisible() const {
return _wbaseEnhancementPanel != kWBASEEnhancementPanelNone;
}
@@ -575,15 +626,24 @@ void BaseGame::pollInput() {
startSelectedAutoplay();
}
break;
- case Common::EVENT_LBUTTONDOWN:
+ case Common::EVENT_MOUSEMOVE:
+ if (_wbaseEnhancementInputArmed && _wbaseEnhancements.controlsEnabled()) {
+ const Common::Point mouse = presentationToWBASEPoint(event.mouse);
+ updateEnhancementPointer(mouse.x, mouse.y);
+ }
+ break;
+ case Common::EVENT_LBUTTONDOWN: {
+ const Common::Point mouse = presentationToWBASEPoint(event.mouse);
+ if (_wbaseEnhancementInputArmed && handleEnhancementControlClick(mouse.x, mouse.y))
+ break;
if (_wbaseEnhancementInputArmed && _wbaseAutoplayMenuInputArmed &&
_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay) {
- const Common::Point mouse = presentationToWBASEPoint(event.mouse);
if (!_wbaseEnhancementsAutoplay.selectMenuPointer(mouse.x, mouse.y))
break;
startSelectedAutoplay();
}
break;
+ }
default:
break;
}
@@ -735,18 +795,6 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
}
}
-void BaseGame::setAutoplayMenuMouseVisible(bool visible) {
- if (_wbaseEnhancementsAutoplayMouseVisible == visible || !_vm->_events)
- return;
- _wbaseEnhancementsAutoplayMouseVisible = visible;
- if (visible) {
- _vm->_events->changeMouseCursor(0);
- _vm->_events->mouseOn();
- } else {
- _vm->_events->mouseOff();
- }
-}
-
void BaseGame::openMainMenu() {
_mainMenuRequested = false;
_input = BaseInputState();
@@ -796,6 +844,8 @@ void BaseGame::renderFrame() {
_wbaseEnhancementsAutoplay.renderStatus(_data, _framebuffer.begin(),
_wbaseEnhancements.forcedAutoplayEnabled());
}
+ _wbaseEnhancements.renderControls(_data, _wbaseEnhancementPanel,
+ _wbaseEnhancementHoveredControl, _wbaseEnhancementsAutoplay.active(), _framebuffer.begin());
Common::copy(_framebuffer.begin(), _framebuffer.end(), _vm->_graphicsMan->_frontBuffer);
_vm->_graphicsMan->addDirtyRect(0, 0, kBaseFrameWidth, kBaseFrameHeight);
_vm->_graphicsMan->updateScreen();
diff --git a/engines/hopkins/base.h b/engines/hopkins/base.h
index f4e3668b805..23d35bc39b2 100644
--- a/engines/hopkins/base.h
+++ b/engines/hopkins/base.h
@@ -88,6 +88,8 @@ private:
void initializeAudio();
void releaseAudio();
void switchKeymaps(bool entering);
+ void initializeEnhancementCursor();
+ void releaseEnhancementCursor();
void resetEnhancementSession();
void updateEnhancementActionState(uint32 action, bool pressed);
void updateEnhancementInputArming(uint32 now);
@@ -95,12 +97,13 @@ private:
void openAutoplayMenu();
void closeNavigationMap();
void startSelectedAutoplay();
+ void updateEnhancementPointer(int x, int y);
+ bool handleEnhancementControlClick(int x, int y);
bool enhancementPanelVisible() const;
void pollInput();
void handleAction(uint32 action, bool pressed);
void openMainMenu();
void refreshPresentation();
- void setAutoplayMenuMouseVisible(bool visible);
void processSoundEvents();
void renderFrame();
void setEngineActive(bool active);
@@ -130,9 +133,10 @@ private:
uint32 _wbaseAutoplayMenuOpenedAt;
int _wbaseForcedAutoplayPromptTicks;
bool _wbaseForcedAutoplayPromptIssued;
+ WBASEEnhancementControl _wbaseEnhancementHoveredControl;
bool _wbaseEnhancementInputArmed;
bool _wbaseAutoplayMenuInputArmed;
- bool _wbaseEnhancementsAutoplayMouseVisible;
+ bool _wbaseEnhancementCursorPushed;
bool _quitRequested;
};
diff --git a/engines/hopkins/base_enhancements.cpp b/engines/hopkins/base_enhancements.cpp
index 024da232ebf..387dfb3fb34 100644
--- a/engines/hopkins/base_enhancements.cpp
+++ b/engines/hopkins/base_enhancements.cpp
@@ -38,6 +38,15 @@ namespace Hopkins {
namespace {
+static const int kMapButtonLeft = 244;
+static const int kMapButtonTop = 182;
+static const int kMapButtonRight = 279;
+static const int kMapButtonBottom = 197;
+static const int kAutoplayButtonLeft = 282;
+static const int kAutoplayButtonTop = 182;
+static const int kAutoplayButtonRight = 317;
+static const int kAutoplayButtonBottom = 197;
+
static byte nearestPaletteColor(const byte *palette, int red, int green, int blue) {
int bestDistance = 0x7fffffff;
byte bestColor = 0;
@@ -88,6 +97,13 @@ static void fillRect(byte *framebuffer, int left, int top, int right, int bottom
drawPixel(framebuffer, x, y, color);
}
+static void drawFrame(byte *framebuffer, int left, int top, int right, int bottom, byte color) {
+ fillRect(framebuffer, left, top, right, top, color);
+ fillRect(framebuffer, left, bottom, right, bottom, color);
+ fillRect(framebuffer, left, top, left, bottom, color);
+ fillRect(framebuffer, right, top, right, bottom, color);
+}
+
static int triangleEdge(int x0, int y0, int x1, int y1, int x, int y) {
return (x - x0) * (y1 - y0) - (y - y0) * (x1 - x0);
}
@@ -110,6 +126,33 @@ static void fillTriangle(byte *framebuffer, int x0, int y0, int x1, int y1,
}
}
+static void drawMapButtonGlyph(byte *framebuffer, int left, int top, byte color) {
+ const int x = left + 4;
+ const int y = top + 4;
+ drawLine(framebuffer, x, y, x + 4, y - 1, color);
+ drawLine(framebuffer, x + 4, y - 1, x + 8, y + 1, color);
+ drawLine(framebuffer, x + 8, y + 1, x + 12, y, color);
+ drawLine(framebuffer, x, y, x, y + 8, color);
+ drawLine(framebuffer, x + 4, y - 1, x + 4, y + 7, color);
+ drawLine(framebuffer, x + 8, y + 1, x + 8, y + 9, color);
+ drawLine(framebuffer, x + 12, y, x + 12, y + 8, color);
+ drawLine(framebuffer, x, y + 8, x + 4, y + 7, color);
+ drawLine(framebuffer, x + 4, y + 7, x + 8, y + 9, color);
+ drawLine(framebuffer, x + 8, y + 9, x + 12, y + 8, color);
+}
+
+static void drawAutoplayButtonGlyph(byte *framebuffer, int left, int top, byte color, bool locked) {
+ fillTriangle(framebuffer, left + 5, top + 4, left + 5, top + 11, left + 11, top + 7, color);
+ if (!locked)
+ return;
+ const int lockX = left + 27;
+ const int lockY = top + 4;
+ drawLine(framebuffer, lockX, lockY + 3, lockX, lockY + 1, color);
+ drawLine(framebuffer, lockX, lockY + 1, lockX + 4, lockY + 1, color);
+ drawLine(framebuffer, lockX + 4, lockY + 1, lockX + 4, lockY + 3, color);
+ fillRect(framebuffer, lockX - 1, lockY + 3, lockX + 5, lockY + 8, color);
+}
+
static void drawPlayerArrow(const BaseData &data, byte *framebuffer, int centerX, int centerY,
int angle, byte fillColor, byte outlineColor) {
const int32 directionX = data.cosQ16(angle);
@@ -198,7 +241,7 @@ static void drawOpaqueLegend(const BaseData &data, byte *framebuffer, byte backg
}
font->drawString(&surface, _("A: autoplay"), 226, 151, 92, textColor,
Graphics::kTextAlignCenter);
- font->drawString(&surface, _("M/Esc: close map"), 0, 183, kBaseFrameWidth, textColor,
+ font->drawString(&surface, _("M/Esc: close map"), 76, 183, 160, textColor,
Graphics::kTextAlignCenter);
}
@@ -297,4 +340,58 @@ void WBASEEnhancements::renderNavigationMap(const BaseData &data, const BaseEngi
renderNavigationMapOpaque(data, engine, framebuffer, colors);
}
+WBASEEnhancementControl WBASEEnhancements::controlAtPoint(int x, int y) const {
+ if (!controlsEnabled())
+ return kWBASEEnhancementControlNone;
+ if (x >= kMapButtonLeft && x <= kMapButtonRight &&
+ y >= kMapButtonTop && y <= kMapButtonBottom)
+ return kWBASEEnhancementControlNavigationMap;
+ if (x >= kAutoplayButtonLeft && x <= kAutoplayButtonRight &&
+ y >= kAutoplayButtonTop && y <= kAutoplayButtonBottom)
+ return kWBASEEnhancementControlAutoplay;
+ return kWBASEEnhancementControlNone;
+}
+
+void WBASEEnhancements::renderControls(const BaseData &data, WBASEEnhancementPanel panel,
+ WBASEEnhancementControl hoveredControl, bool autoplayActive, byte *framebuffer) const {
+ if (!controlsEnabled() || !framebuffer)
+ return;
+
+ Graphics::Surface surface;
+ surface.init(kBaseFrameWidth, kBaseFrameHeight, kBaseFrameWidth, framebuffer,
+ Graphics::PixelFormat::createFormatCLUT8());
+ const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
+ if (!font)
+ return;
+
+ const byte *palette = data.palette();
+ const byte background = nearestPaletteColor(palette, 0, 0, 0);
+ const byte idle = nearestPaletteColor(palette, 105, 105, 105);
+ const byte hover = nearestPaletteColor(palette, 255, 255, 255);
+ const byte mapActive = nearestPaletteColor(palette, 0, 220, 255);
+ const byte chooserActive = nearestPaletteColor(palette, 255, 220, 0);
+ const byte autoplayRunning = nearestPaletteColor(palette, 40, 255, 60);
+
+ const byte mapColor = panel == kWBASEEnhancementPanelNavigationMap ? mapActive : idle;
+ const byte autoplayColor = panel == kWBASEEnhancementPanelAutoplay ? chooserActive :
+ (autoplayActive ? autoplayRunning : idle);
+ const byte mapBorder = hoveredControl == kWBASEEnhancementControlNavigationMap ? hover : mapColor;
+ const byte autoplayBorder = hoveredControl == kWBASEEnhancementControlAutoplay ? hover : autoplayColor;
+
+ fillRect(framebuffer, kMapButtonLeft, kMapButtonTop, kMapButtonRight, kMapButtonBottom, background);
+ drawFrame(framebuffer, kMapButtonLeft, kMapButtonTop, kMapButtonRight, kMapButtonBottom, mapBorder);
+ drawMapButtonGlyph(framebuffer, kMapButtonLeft, kMapButtonTop, mapColor);
+ font->drawString(&surface, Common::String("M"), kMapButtonLeft + 22, kMapButtonTop + 4, 9, mapColor,
+ Graphics::kTextAlignCenter);
+
+ fillRect(framebuffer, kAutoplayButtonLeft, kAutoplayButtonTop,
+ kAutoplayButtonRight, kAutoplayButtonBottom, background);
+ drawFrame(framebuffer, kAutoplayButtonLeft, kAutoplayButtonTop,
+ kAutoplayButtonRight, kAutoplayButtonBottom, autoplayBorder);
+ drawAutoplayButtonGlyph(framebuffer, kAutoplayButtonLeft, kAutoplayButtonTop,
+ autoplayColor, _forcedAutoplay);
+ font->drawString(&surface, Common::String("A"), kAutoplayButtonLeft + 15, kAutoplayButtonTop + 4, 9,
+ autoplayColor, Graphics::kTextAlignCenter);
+}
+
} // End of namespace Hopkins
diff --git a/engines/hopkins/base_enhancements.h b/engines/hopkins/base_enhancements.h
index 81b13059a1b..0c433339333 100644
--- a/engines/hopkins/base_enhancements.h
+++ b/engines/hopkins/base_enhancements.h
@@ -40,6 +40,12 @@ enum WBASEEnhancementPanel {
kWBASEEnhancementPanelAutoplay
};
+enum WBASEEnhancementControl {
+ kWBASEEnhancementControlNone,
+ kWBASEEnhancementControlNavigationMap,
+ kWBASEEnhancementControlAutoplay
+};
+
class WBASEEnhancements {
public:
explicit WBASEEnhancements(const Common::String &targetName);
@@ -49,6 +55,9 @@ public:
bool controlsEnabled() const { return _enabled || _forcedAutoplay; }
bool navigationMapEnabled() const { return controlsEnabled(); }
void renderNavigationMap(const BaseData &data, const BaseEngine &engine, byte *framebuffer) const;
+ WBASEEnhancementControl controlAtPoint(int x, int y) const;
+ void renderControls(const BaseData &data, WBASEEnhancementPanel panel,
+ WBASEEnhancementControl hoveredControl, bool autoplayActive, byte *framebuffer) const;
private:
bool _enabled;
Commit: 48d38249efcb2aca68df1cac2ed0e1d321cf72ac
https://github.com/scummvm/scummvm/commit/48d38249efcb2aca68df1cac2ed0e1d321cf72ac
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: Validate debugger teleport destinations
Hopkins uses sparse room identifiers. The teleport command previously
accepted any integer, so an unhandled value left the main room
dispatcher spinning without polling events, sometimes crashing.
Fixes e.g. "teleport 2", "teleport 999".
Changed paths:
engines/hopkins/debugger.cpp
engines/hopkins/hopkins.cpp
engines/hopkins/hopkins.h
diff --git a/engines/hopkins/debugger.cpp b/engines/hopkins/debugger.cpp
index 6d9250f924f..4039c9c7d35 100644
--- a/engines/hopkins/debugger.cpp
+++ b/engines/hopkins/debugger.cpp
@@ -54,7 +54,13 @@ bool Debugger::cmd_Teleport(int argc, const char **argv) {
debugPrintf("%s: [Room number]\n", argv[0]);
return true;
} else {
- _vm->_globals->_exitId = atoi(argv[1]);
+ const int roomId = atoi(argv[1]);
+ if (!_vm->canTeleportToRoom(roomId)) {
+ debugPrintf("Room %d is not available in this game variant\n", roomId);
+ return true;
+ }
+
+ _vm->_globals->_exitId = roomId;
return false;
}
}
diff --git a/engines/hopkins/hopkins.cpp b/engines/hopkins/hopkins.cpp
index f85df21545a..5ccafe19cbc 100644
--- a/engines/hopkins/hopkins.cpp
+++ b/engines/hopkins/hopkins.cpp
@@ -105,6 +105,37 @@ Common::Error HopkinsEngine::saveGameState(int slot, const Common::String &desc,
return _saveLoad->saveGame(slot, desc);
}
+bool HopkinsEngine::canTeleportToRoom(int roomId) const {
+ if (!getIsDemo()) {
+ return roomId == 1 ||
+ (roomId >= 3 && roomId <= 20) ||
+ (roomId >= 22 && roomId <= 41) ||
+ (roomId >= 50 && roomId <= 52) ||
+ (roomId >= 54 && roomId <= 71) ||
+ roomId == 73 || roomId == 75 ||
+ (roomId >= 77 && roomId <= 91) ||
+ (roomId >= 93 && roomId <= 100) ||
+ (roomId >= 111 && roomId <= 115) ||
+ (roomId >= 150 && roomId <= 152) ||
+ (roomId >= 194 && roomId <= 199);
+ }
+
+ if (getPlatform() == Common::kPlatformWindows) {
+ return roomId == 1 ||
+ (roomId >= 3 && roomId <= 17) ||
+ (roomId >= 24 && roomId <= 34) ||
+ (roomId >= 111 && roomId <= 115) ||
+ (roomId >= 150 && roomId <= 152);
+ }
+
+ return roomId == 1 ||
+ (roomId >= 3 && roomId <= 20) ||
+ (roomId >= 22 && roomId <= 35) ||
+ roomId == 38 ||
+ (roomId >= 111 && roomId <= 115) ||
+ (roomId >= 150 && roomId <= 152);
+}
+
Common::Error HopkinsEngine::run() {
_globals->setConfig();
_fileIO->initCensorship();
diff --git a/engines/hopkins/hopkins.h b/engines/hopkins/hopkins.h
index 75beb062e0b..b520286308c 100644
--- a/engines/hopkins/hopkins.h
+++ b/engines/hopkins/hopkins.h
@@ -97,12 +97,14 @@ class BaseGame;
class HopkinsEngine : public Engine {
private:
friend class BaseGame;
+ friend class Debugger;
const HopkinsGameDescription *_gameDescription;
Common::RandomSource _randomSource;
bool _inBaseGame;
void initializeSystem();
+ bool canTeleportToRoom(int roomId) const;
void displayNotAvailable();
void restoreSystem();
Commit: 09eb971be5128dc1b07845f4fcb81b1fc8572e17
https://github.com/scummvm/scummvm/commit/09eb971be5128dc1b07845f4fcb81b1fc8572e17
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: Fix STP scanner classification
STP already has a runtime handler, but conditional scanner omitted it.
This produced false unhandled-opcode warnings while reading IM01.INI
inside RES_INI.RES: "Unhandled opcode STP!" (Hopkins UK CD)
Changed paths:
engines/hopkins/script.cpp
diff --git a/engines/hopkins/script.cpp b/engines/hopkins/script.cpp
index f0b76421d57..9921132829c 100644
--- a/engines/hopkins/script.cpp
+++ b/engines/hopkins/script.cpp
@@ -2651,6 +2651,7 @@ int ScriptManager::checkOpcode(const byte *dataP) {
case MKTAG24('P', 'E', 'R'):
case MKTAG24('S', 'O', 'U'):
case MKTAG24('S', 'P', 'E'):
+ case MKTAG24('S', 'T', 'P'):
case MKTAG24('T', 'X', 'T'):
case MKTAG24('V', 'A', 'L'):
case MKTAG24('V', 'O', 'F'):
Commit: 3f70cd94f808223bf998d17a2c266e8c8895c15a
https://github.com/scummvm/scummvm/commit/3f70cd94f808223bf998d17a2c266e8c8895c15a
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: WBASE: Keep cursor only in HUD and chooser
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/hopkins/base.cpp
engines/hopkins/base.h
diff --git a/engines/hopkins/base.cpp b/engines/hopkins/base.cpp
index 2d4aefc1f4e..48524b03f15 100644
--- a/engines/hopkins/base.cpp
+++ b/engines/hopkins/base.cpp
@@ -58,11 +58,17 @@ static uint32 enhancementActionMask(uint32 action) {
}
// WBASE renders a 320x200 frame through the original Hopkins 640x480
-// presentation: 2x pixels with 30-pixel top and bottom borders. Backend mouse
-// events remain in that 640x480 coordinate space, so menu hit testing must undo
-// the presentation transform.
-static Common::Point presentationToWBASEPoint(const Common::Point &point) {
- return Common::Point(point.x / 2, (point.y - 30) / 2);
+// presentation. Backend mouse events already use that virtual coordinate
+// space, so only the presentation offset and scale need to be removed.
+static bool presentationToWBASEPoint(const Common::Point &point, Common::Point &result) {
+ if (point.x < 0 || point.x >= 640 || point.y < 30 || point.y >= 430)
+ return false;
+ result = Common::Point(point.x / 2, (point.y - 30) / 2);
+ return true;
+}
+
+static bool autoplayMenuContainsPoint(const Common::Point &point) {
+ return point.x >= 42 && point.x <= 277 && point.y >= 25 && point.y <= 180;
}
static const char *const kBaseSounds[] = {
@@ -146,7 +152,7 @@ BaseGame::BaseGame(HopkinsEngine *vm) :
_wbaseAutoplayMenuOpenedAt(0), _wbaseForcedAutoplayPromptTicks(0),
_wbaseForcedAutoplayPromptIssued(false), _wbaseEnhancementHoveredControl(kWBASEEnhancementControlNone),
_wbaseEnhancementInputArmed(false), _wbaseAutoplayMenuInputArmed(false),
- _wbaseEnhancementCursorPushed(false),
+ _wbaseEnhancementCursorPushed(false), _wbaseEnhancementCursorVisible(false),
_quitRequested(false) {
_framebuffer.resize(kBaseFrameWidth * kBaseFrameHeight);
Common::fill(_audioLoaded, _audioLoaded + ARRAYSIZE(_audioLoaded), false);
@@ -240,8 +246,11 @@ BaseRunResult BaseGame::run(int entryId) {
previousTime = now;
accumulator = 0;
if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay && _wbaseAutoplayMenuInputArmed) {
- const Common::Point mouse = presentationToWBASEPoint(g_system->getEventManager()->getMousePos());
- _wbaseEnhancementsAutoplay.updateMenuPointer(mouse.x, mouse.y);
+ Common::Point mouse;
+ if (presentationToWBASEPoint(g_system->getEventManager()->getMousePos(), mouse))
+ _wbaseEnhancementsAutoplay.updateMenuPointer(mouse.x, mouse.y);
+ else
+ _wbaseEnhancementsAutoplay.clearMenuPointer();
}
if (enhancementPanelVisible() && !_inputSuspended)
renderFrame();
@@ -442,10 +451,12 @@ void BaseGame::initializeEnhancementCursor() {
if (!_wbaseEnhancements.controlsEnabled() || _wbaseEnhancementCursorPushed)
return;
CursorMan.setDefaultArrowCursor(true);
- CursorMan.showMouse(true);
- if (_vm->_events)
- _vm->_events->_mouseFl = true;
_wbaseEnhancementCursorPushed = true;
+ _wbaseEnhancementCursorVisible = false;
+ CursorMan.showMouse(false);
+ if (_vm->_events)
+ _vm->_events->_mouseFl = false;
+ updateEnhancementCursor();
}
void BaseGame::releaseEnhancementCursor() {
@@ -457,6 +468,27 @@ void BaseGame::releaseEnhancementCursor() {
if (_vm->_events)
_vm->_events->_mouseFl = false;
_wbaseEnhancementCursorPushed = false;
+ _wbaseEnhancementCursorVisible = false;
+}
+
+void BaseGame::updateEnhancementCursor() {
+ if (!_wbaseEnhancementCursorPushed || g_system->isOverlayVisible())
+ return;
+
+ Common::Point mouse;
+ bool visible = false;
+ if (!_inputSuspended && presentationToWBASEPoint(g_system->getEventManager()->getMousePos(), mouse)) {
+ visible = mouse.y >= kBaseViewHeight;
+ if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay)
+ visible = visible || autoplayMenuContainsPoint(mouse);
+ }
+
+ if (visible != _wbaseEnhancementCursorVisible) {
+ CursorMan.showMouse(visible);
+ _wbaseEnhancementCursorVisible = visible;
+ }
+ if (_vm->_events)
+ _vm->_events->_mouseFl = visible;
}
void BaseGame::resetEnhancementSession() {
@@ -628,12 +660,17 @@ void BaseGame::pollInput() {
break;
case Common::EVENT_MOUSEMOVE:
if (_wbaseEnhancementInputArmed && _wbaseEnhancements.controlsEnabled()) {
- const Common::Point mouse = presentationToWBASEPoint(event.mouse);
- updateEnhancementPointer(mouse.x, mouse.y);
+ Common::Point mouse;
+ if (presentationToWBASEPoint(event.mouse, mouse))
+ updateEnhancementPointer(mouse.x, mouse.y);
+ else
+ updateEnhancementPointer(-1, -1);
}
break;
case Common::EVENT_LBUTTONDOWN: {
- const Common::Point mouse = presentationToWBASEPoint(event.mouse);
+ Common::Point mouse;
+ if (!presentationToWBASEPoint(event.mouse, mouse))
+ break;
if (_wbaseEnhancementInputArmed && handleEnhancementControlClick(mouse.x, mouse.y))
break;
if (_wbaseEnhancementInputArmed && _wbaseAutoplayMenuInputArmed &&
@@ -649,6 +686,7 @@ void BaseGame::pollInput() {
}
}
updateEnhancementInputArming(g_system->getMillis());
+ updateEnhancementCursor();
if (_mainMenuRequested && _result == -1 && !_quitRequested && !_vm->shouldQuit())
openMainMenu();
@@ -848,6 +886,7 @@ void BaseGame::renderFrame() {
_wbaseEnhancementHoveredControl, _wbaseEnhancementsAutoplay.active(), _framebuffer.begin());
Common::copy(_framebuffer.begin(), _framebuffer.end(), _vm->_graphicsMan->_frontBuffer);
_vm->_graphicsMan->addDirtyRect(0, 0, kBaseFrameWidth, kBaseFrameHeight);
+ updateEnhancementCursor();
_vm->_graphicsMan->updateScreen();
}
diff --git a/engines/hopkins/base.h b/engines/hopkins/base.h
index 23d35bc39b2..372e85773da 100644
--- a/engines/hopkins/base.h
+++ b/engines/hopkins/base.h
@@ -90,6 +90,7 @@ private:
void switchKeymaps(bool entering);
void initializeEnhancementCursor();
void releaseEnhancementCursor();
+ void updateEnhancementCursor();
void resetEnhancementSession();
void updateEnhancementActionState(uint32 action, bool pressed);
void updateEnhancementInputArming(uint32 now);
@@ -137,6 +138,7 @@ private:
bool _wbaseEnhancementInputArmed;
bool _wbaseAutoplayMenuInputArmed;
bool _wbaseEnhancementCursorPushed;
+ bool _wbaseEnhancementCursorVisible;
bool _quitRequested;
};
Commit: 264d124d831cdbef042304522d76cdcaaae50009
https://github.com/scummvm/scummvm/commit/264d124d831cdbef042304522d76cdcaaae50009
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: WBASE: Preserve input until the next game tick
Quick taps can otherwise begin and end between two simulation ticks.
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/hopkins/base.cpp
engines/hopkins/base.h
diff --git a/engines/hopkins/base.cpp b/engines/hopkins/base.cpp
index 48524b03f15..cfb03c68c9e 100644
--- a/engines/hopkins/base.cpp
+++ b/engines/hopkins/base.cpp
@@ -143,7 +143,9 @@ private:
};
BaseGame::BaseGame(HopkinsEngine *vm) :
- _vm(vm), _wbaseEnhancements(vm->getTargetName()), _engine(nullptr), _renderer(nullptr), _entry(nullptr), _result(-1),
+ _vm(vm), _wbaseEnhancements(vm->getTargetName()), _engine(nullptr), _renderer(nullptr),
+ _wbaseMappedInputHolds(0), _wbaseMappedInputPulses(0), _wbaseInputGeneration(0),
+ _wbaseTextureTogglePending(false), _entry(nullptr), _result(-1),
_keymapsSwitched(false), _defaultKeymapWasEnabled(false),
_shortcutKeymapWasEnabled(false), _baseKeymapWasEnabled(false), _wbaseEnhancementsKeymapWasEnabled(false),
_inputSuspended(false), _mainMenuRequested(false),
@@ -153,7 +155,7 @@ BaseGame::BaseGame(HopkinsEngine *vm) :
_wbaseForcedAutoplayPromptIssued(false), _wbaseEnhancementHoveredControl(kWBASEEnhancementControlNone),
_wbaseEnhancementInputArmed(false), _wbaseAutoplayMenuInputArmed(false),
_wbaseEnhancementCursorPushed(false), _wbaseEnhancementCursorVisible(false),
- _quitRequested(false) {
+ _wbaseOverlayVisible(false), _quitRequested(false) {
_framebuffer.resize(kBaseFrameWidth * kBaseFrameHeight);
Common::fill(_audioLoaded, _audioLoaded + ARRAYSIZE(_audioLoaded), false);
}
@@ -265,8 +267,9 @@ BaseRunResult BaseGame::run(int entryId) {
bool advanced = false;
while (accumulator >= 1000 && _result == -1) {
accumulator -= 1000;
+ BaseInputState gameplayInput;
BaseInputState autoplayInput;
- BaseInputState *tickInput = &_input;
+ BaseInputState *tickInput = &gameplayInput;
if (_wbaseEnhancementsAutoplay.active()) {
const int forcedRoom = _wbaseEnhancementsAutoplay.update(*_engine, autoplayInput);
if (forcedRoom >= 94 && forcedRoom <= 99) {
@@ -281,6 +284,8 @@ BaseRunResult BaseGame::run(int entryId) {
tickInput = &autoplayInput;
} else if (_wbaseEnhancements.forcedAutoplayEnabled()) {
tickInput = &autoplayInput;
+ } else {
+ gameplayInput = consumeGameplayInput();
}
const int tickResult = _engine->tick(*tickInput);
processSoundEvents();
@@ -492,30 +497,90 @@ void BaseGame::updateEnhancementCursor() {
}
void BaseGame::resetEnhancementSession() {
- _input = BaseInputState();
+ clearGameplayInput(true);
_wbaseEnhancementPanel = kWBASEEnhancementPanelNone;
- _wbaseEnhancementHeldActions = 0;
_wbaseAutoplayMenuOpenedAt = 0;
_wbaseForcedAutoplayPromptTicks = _wbaseEnhancements.forcedAutoplayEnabled() ? kForcedAutoplayPromptTicks : 0;
_wbaseForcedAutoplayPromptIssued = false;
_wbaseEnhancementHoveredControl = kWBASEEnhancementControlNone;
_wbaseEnhancementInputArmed = !_wbaseEnhancements.controlsEnabled();
_wbaseAutoplayMenuInputArmed = false;
+ _wbaseOverlayVisible = g_system->isOverlayVisible();
_wbaseEnhancementsAutoplay.reset();
}
-void BaseGame::updateEnhancementActionState(uint32 action, bool pressed) {
+void BaseGame::clearGameplayInput(bool disarm) {
+ _wbaseMappedInputHolds = 0;
+ _wbaseMappedInputPulses = 0;
+ _wbaseTextureTogglePending = false;
+ _wbasePendingGameplayRequests.clear();
+ ++_wbaseInputGeneration;
+ if (_wbaseInputGeneration == 0)
+ ++_wbaseInputGeneration;
+ if (disarm) {
+ _wbaseEnhancementHeldActions = 0;
+ _wbaseEnhancementInputArmed = !_wbaseEnhancements.controlsEnabled();
+ _wbaseAutoplayMenuInputArmed = false;
+ }
+}
+
+BaseInputState BaseGame::consumeGameplayInput() {
+ const uint32 active = _wbaseMappedInputHolds | _wbaseMappedInputPulses;
+ BaseInputState input;
+ input.forward = (active & enhancementActionMask(kActionBaseForward)) != 0;
+ input.backward = (active & enhancementActionMask(kActionBaseBackward)) != 0;
+ input.turnLeft = (active & enhancementActionMask(kActionBaseTurnLeft)) != 0;
+ input.turnRight = (active & enhancementActionMask(kActionBaseTurnRight)) != 0;
+ input.fire = (active & enhancementActionMask(kActionBaseFire)) != 0;
+ input.toggleTextures = _wbaseTextureTogglePending;
+ _wbaseMappedInputPulses = 0;
+ _wbaseTextureTogglePending = false;
+
+ while (!_wbasePendingGameplayRequests.empty() &&
+ _wbasePendingGameplayRequests.front().generation != _wbaseInputGeneration)
+ _wbasePendingGameplayRequests.pop();
+ if (!_wbasePendingGameplayRequests.empty()) {
+ const PendingGameplayRequest request = _wbasePendingGameplayRequests.pop();
+ if (request.type == kPendingGameplayExit)
+ input.exitRequested = true;
+ }
+ return input;
+}
+
+void BaseGame::setMappedGameplayAction(uint32 action, bool pressed) {
+ if (action < kActionBaseForward || action > kActionBaseFire)
+ return;
+ const uint32 mask = enhancementActionMask(action);
+ if (pressed) {
+ if (_wbaseEnhancements.controlsEnabled() && !(_wbaseMappedInputHolds & mask))
+ _wbaseMappedInputPulses |= mask;
+ _wbaseMappedInputHolds |= mask;
+ } else {
+ _wbaseMappedInputHolds &= ~mask;
+ }
+}
+
+void BaseGame::queueGameplayRequest(PendingGameplayRequestType type) {
+ PendingGameplayRequest request;
+ request.type = type;
+ request.generation = _wbaseInputGeneration;
+ _wbasePendingGameplayRequests.push(request);
+}
+
+bool BaseGame::updateEnhancementActionState(uint32 action, bool pressed) {
const uint32 mask = enhancementActionMask(action);
if (!mask)
- return;
+ return true;
+ const bool wasPressed = (_wbaseEnhancementHeldActions & mask) != 0;
if (pressed)
_wbaseEnhancementHeldActions |= mask;
else
_wbaseEnhancementHeldActions &= ~mask;
+ return wasPressed != pressed;
}
void BaseGame::updateEnhancementInputArming(uint32 now) {
- if (_inputSuspended)
+ if (_inputSuspended || _wbaseOverlayVisible)
return;
const bool mouseReleased = !(g_system->getEventManager()->getButtonState() & Common::EventManager::LBUTTON);
if (!_wbaseEnhancementInputArmed && !_wbaseEnhancementHeldActions && mouseReleased) {
@@ -531,9 +596,18 @@ void BaseGame::updateEnhancementInputArming(uint32 now) {
}
}
+void BaseGame::updateOverlayInputState() {
+ const bool overlayVisible = g_system->isOverlayVisible();
+ if (overlayVisible == _wbaseOverlayVisible)
+ return;
+ _wbaseOverlayVisible = overlayVisible;
+ clearGameplayInput(true);
+ _timingResetRequested = true;
+}
+
void BaseGame::setEnhancementPanel(WBASEEnhancementPanel panel) {
_wbaseEnhancementPanel = panel;
- _input = BaseInputState();
+ clearGameplayInput(false);
_timingResetRequested = true;
_wbaseEnhancementsAutoplay.clearMenuPointer();
_wbaseAutoplayMenuInputArmed = false;
@@ -605,39 +679,41 @@ bool BaseGame::enhancementPanelVisible() const {
}
void BaseGame::pollInput() {
+ updateOverlayInputState();
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
switch (event.type) {
case Common::EVENT_QUIT:
case Common::EVENT_RETURN_TO_LAUNCHER:
- _input = BaseInputState();
+ clearGameplayInput(true);
_quitRequested = true;
break;
case Common::EVENT_MAINMENU:
_mainMenuRequested = true;
break;
case Common::EVENT_SCREEN_CHANGED:
- _input = BaseInputState();
+ clearGameplayInput(true);
_presentationRefreshRequested = true;
_timingResetRequested = true;
break;
case Common::EVENT_CUSTOM_ENGINE_ACTION_START: {
- updateEnhancementActionState(event.customType, true);
- if (_wbaseEnhancementInputArmed)
- handleAction(event.customType, true);
- else
- debug(2, "Hopkins WBASE suppressed entry-boundary action %u", event.customType);
+ const bool changed = updateEnhancementActionState(event.customType, true);
+ if (changed) {
+ if (_wbaseEnhancementInputArmed)
+ handleAction(event.customType, true);
+ else
+ debug(2, "Hopkins WBASE suppressed entry-boundary action %u", event.customType);
+ }
break;
}
- case Common::EVENT_CUSTOM_ENGINE_ACTION_END:
- updateEnhancementActionState(event.customType, false);
- if (_wbaseEnhancementInputArmed)
+ case Common::EVENT_CUSTOM_ENGINE_ACTION_END: {
+ const bool changed = updateEnhancementActionState(event.customType, false);
+ if (_wbaseEnhancementInputArmed && changed)
handleAction(event.customType, false);
break;
+ }
case Common::EVENT_FOCUS_LOST:
- _input = BaseInputState();
- _wbaseEnhancementHeldActions = 0;
- _wbaseAutoplayMenuInputArmed = false;
+ clearGameplayInput(true);
_inputSuspended = true;
_timingResetRequested = true;
break;
@@ -646,8 +722,7 @@ void BaseGame::pollInput() {
_timingResetRequested = true;
break;
case Common::EVENT_INPUT_CHANGED:
- _input = BaseInputState();
- _wbaseEnhancementHeldActions = 0;
+ clearGameplayInput(true);
_timingResetRequested = true;
break;
case Common::EVENT_KEYDOWN:
@@ -685,6 +760,7 @@ void BaseGame::pollInput() {
break;
}
}
+ updateOverlayInputState();
updateEnhancementInputArming(g_system->getMillis());
updateEnhancementCursor();
@@ -731,7 +807,7 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
default:
return;
}
- _input = BaseInputState();
+ clearGameplayInput(false);
_timingResetRequested = true;
renderFrame();
return;
@@ -785,7 +861,7 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
case kActionBaseFire:
case kActionBaseUse:
_wbaseEnhancementsAutoplay.cancel();
- _input = BaseInputState();
+ clearGameplayInput(false);
break;
default:
break;
@@ -794,27 +870,19 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
switch (action) {
case kActionBaseForward:
- _input.forward = pressed;
- break;
case kActionBaseBackward:
- _input.backward = pressed;
- break;
case kActionBaseTurnLeft:
- _input.turnLeft = pressed;
- break;
case kActionBaseTurnRight:
- _input.turnRight = pressed;
- break;
case kActionBaseFire:
- _input.fire = pressed;
+ setMappedGameplayAction(action, pressed);
break;
case kActionBaseUse:
if (pressed)
- _input.exitRequested = true;
+ queueGameplayRequest(kPendingGameplayExit);
break;
case kActionBaseToggleTextures:
if (pressed)
- _input.toggleTextures = true;
+ _wbaseTextureTogglePending = true;
break;
case kActionBaseMenu:
if (pressed)
@@ -835,13 +903,15 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
void BaseGame::openMainMenu() {
_mainMenuRequested = false;
- _input = BaseInputState();
+ clearGameplayInput(true);
_inputSuspended = true;
_vm->openMainMenuDialog();
// A modal menu may consume the KEYUP for the key that opened it. Purge
// keyboard events before resuming so WBASE cannot inherit a stuck action.
g_system->getEventManager()->purgeKeyboardEvents();
_inputSuspended = false;
+ _wbaseOverlayVisible = g_system->isOverlayVisible();
+ clearGameplayInput(true);
_timingResetRequested = true;
if (_vm->shouldQuit())
diff --git a/engines/hopkins/base.h b/engines/hopkins/base.h
index 372e85773da..c3c56892ebf 100644
--- a/engines/hopkins/base.h
+++ b/engines/hopkins/base.h
@@ -30,6 +30,7 @@
#include "hopkins/base_enhancements_autoplay.h"
#include "common/array.h"
+#include "common/queue.h"
#include "common/scummsys.h"
#include "common/str.h"
@@ -79,6 +80,13 @@ private:
};
class SessionGuard;
+ enum PendingGameplayRequestType {
+ kPendingGameplayExit
+ };
+ struct PendingGameplayRequest {
+ PendingGameplayRequestType type;
+ uint32 generation;
+ };
static const BaseEntryPoint *entryPoints(uint &count);
static const BaseEntryPoint *findEntryPoint(int entryId);
@@ -92,8 +100,13 @@ private:
void releaseEnhancementCursor();
void updateEnhancementCursor();
void resetEnhancementSession();
- void updateEnhancementActionState(uint32 action, bool pressed);
+ void clearGameplayInput(bool disarm);
+ BaseInputState consumeGameplayInput();
+ void setMappedGameplayAction(uint32 action, bool pressed);
+ void queueGameplayRequest(PendingGameplayRequestType type);
+ bool updateEnhancementActionState(uint32 action, bool pressed);
void updateEnhancementInputArming(uint32 now);
+ void updateOverlayInputState();
void setEnhancementPanel(WBASEEnhancementPanel panel);
void openAutoplayMenu();
void closeNavigationMap();
@@ -116,7 +129,11 @@ private:
BaseEngine *_engine;
BaseRenderer *_renderer;
Common::Array<byte> _framebuffer;
- BaseInputState _input;
+ uint32 _wbaseMappedInputHolds;
+ uint32 _wbaseMappedInputPulses;
+ Common::Queue<PendingGameplayRequest> _wbasePendingGameplayRequests;
+ uint32 _wbaseInputGeneration;
+ bool _wbaseTextureTogglePending;
const BaseEntryPoint *_entry;
int _result;
bool _audioLoaded[6];
@@ -139,6 +156,7 @@ private:
bool _wbaseAutoplayMenuInputArmed;
bool _wbaseEnhancementCursorPushed;
bool _wbaseEnhancementCursorVisible;
+ bool _wbaseOverlayVisible;
bool _quitRequested;
};
Commit: bc23974b183c6f04a26357b50faa31c1537b6d18
https://github.com/scummvm/scummvm/commit/bc23974b183c6f04a26357b50faa31c1537b6d18
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: WBASE: Add compact gameplay controls to the HUD
This gives mouse and touch users direct access to WBASE actions.
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/hopkins/base.cpp
engines/hopkins/base.h
engines/hopkins/base_enhancements.cpp
engines/hopkins/base_enhancements.h
diff --git a/engines/hopkins/base.cpp b/engines/hopkins/base.cpp
index cfb03c68c9e..91a08aaf827 100644
--- a/engines/hopkins/base.cpp
+++ b/engines/hopkins/base.cpp
@@ -144,7 +144,8 @@ private:
BaseGame::BaseGame(HopkinsEngine *vm) :
_vm(vm), _wbaseEnhancements(vm->getTargetName()), _engine(nullptr), _renderer(nullptr),
- _wbaseMappedInputHolds(0), _wbaseMappedInputPulses(0), _wbaseInputGeneration(0),
+ _wbaseMappedInputHolds(0), _wbaseMappedInputPulses(0),
+ _wbasePointerInputHolds(0), _wbasePointerInputPulses(0), _wbaseInputGeneration(0),
_wbaseTextureTogglePending(false), _entry(nullptr), _result(-1),
_keymapsSwitched(false), _defaultKeymapWasEnabled(false),
_shortcutKeymapWasEnabled(false), _baseKeymapWasEnabled(false), _wbaseEnhancementsKeymapWasEnabled(false),
@@ -155,7 +156,9 @@ BaseGame::BaseGame(HopkinsEngine *vm) :
_wbaseForcedAutoplayPromptIssued(false), _wbaseEnhancementHoveredControl(kWBASEEnhancementControlNone),
_wbaseEnhancementInputArmed(false), _wbaseAutoplayMenuInputArmed(false),
_wbaseEnhancementCursorPushed(false), _wbaseEnhancementCursorVisible(false),
- _wbaseOverlayVisible(false), _quitRequested(false) {
+ _wbaseOverlayVisible(false), _wbasePointerButtonDown(false),
+ _wbasePointerCapture(kPointerCaptureNone),
+ _wbasePointerCapturedControl(kWBASEEnhancementControlNone), _quitRequested(false) {
_framebuffer.resize(kBaseFrameWidth * kBaseFrameHeight);
Common::fill(_audioLoaded, _audioLoaded + ARRAYSIZE(_audioLoaded), false);
}
@@ -509,15 +512,27 @@ void BaseGame::resetEnhancementSession() {
_wbaseEnhancementsAutoplay.reset();
}
-void BaseGame::clearGameplayInput(bool disarm) {
+void BaseGame::clearLogicalGameplayInput() {
_wbaseMappedInputHolds = 0;
_wbaseMappedInputPulses = 0;
+ _wbasePointerInputHolds = 0;
+ _wbasePointerInputPulses = 0;
_wbaseTextureTogglePending = false;
_wbasePendingGameplayRequests.clear();
++_wbaseInputGeneration;
if (_wbaseInputGeneration == 0)
++_wbaseInputGeneration;
+}
+
+void BaseGame::clearGameplayInput(bool disarm) {
+ clearLogicalGameplayInput();
+ if (_wbasePointerButtonDown && !disarm)
+ _wbasePointerCapture = kPointerCaptureIgnored;
+ else
+ _wbasePointerCapture = kPointerCaptureNone;
+ _wbasePointerCapturedControl = kWBASEEnhancementControlNone;
if (disarm) {
+ _wbasePointerButtonDown = false;
_wbaseEnhancementHeldActions = 0;
_wbaseEnhancementInputArmed = !_wbaseEnhancements.controlsEnabled();
_wbaseAutoplayMenuInputArmed = false;
@@ -525,7 +540,8 @@ void BaseGame::clearGameplayInput(bool disarm) {
}
BaseInputState BaseGame::consumeGameplayInput() {
- const uint32 active = _wbaseMappedInputHolds | _wbaseMappedInputPulses;
+ const uint32 active = _wbaseMappedInputHolds | _wbaseMappedInputPulses |
+ _wbasePointerInputHolds | _wbasePointerInputPulses;
BaseInputState input;
input.forward = (active & enhancementActionMask(kActionBaseForward)) != 0;
input.backward = (active & enhancementActionMask(kActionBaseBackward)) != 0;
@@ -534,6 +550,7 @@ BaseInputState BaseGame::consumeGameplayInput() {
input.fire = (active & enhancementActionMask(kActionBaseFire)) != 0;
input.toggleTextures = _wbaseTextureTogglePending;
_wbaseMappedInputPulses = 0;
+ _wbasePointerInputPulses = 0;
_wbaseTextureTogglePending = false;
while (!_wbasePendingGameplayRequests.empty() &&
@@ -560,13 +577,46 @@ void BaseGame::setMappedGameplayAction(uint32 action, bool pressed) {
}
}
-void BaseGame::queueGameplayRequest(PendingGameplayRequestType type) {
+void BaseGame::setPointerGameplayAction(uint32 action, bool pressed, bool cancelPulse) {
+ if (action < kActionBaseForward || action > kActionBaseFire)
+ return;
+ const uint32 mask = enhancementActionMask(action);
+ if (pressed) {
+ if (!(_wbasePointerInputHolds & mask))
+ _wbasePointerInputPulses |= mask;
+ _wbasePointerInputHolds |= mask;
+ } else {
+ _wbasePointerInputHolds &= ~mask;
+ if (cancelPulse)
+ _wbasePointerInputPulses &= ~mask;
+ }
+}
+
+void BaseGame::cancelAutoplayForManualInput() {
+ if (!_wbaseEnhancementsAutoplay.active())
+ return;
+ _wbaseEnhancementsAutoplay.cancel();
+ clearLogicalGameplayInput();
+}
+
+void BaseGame::queueGameplayRequest(PendingGameplayRequestType type,
+ PendingGameplayRequestSource source) {
PendingGameplayRequest request;
request.type = type;
+ request.source = source;
request.generation = _wbaseInputGeneration;
_wbasePendingGameplayRequests.push(request);
}
+void BaseGame::cancelPointerGameplayRequests() {
+ const int requestCount = _wbasePendingGameplayRequests.size();
+ for (int i = 0; i < requestCount; ++i) {
+ const PendingGameplayRequest request = _wbasePendingGameplayRequests.pop();
+ if (request.source != kPendingGameplayPointer)
+ _wbasePendingGameplayRequests.push(request);
+ }
+}
+
bool BaseGame::updateEnhancementActionState(uint32 action, bool pressed) {
const uint32 mask = enhancementActionMask(action);
if (!mask)
@@ -655,23 +705,142 @@ void BaseGame::updateEnhancementPointer(int x, int y) {
renderFrame();
}
-bool BaseGame::handleEnhancementControlClick(int x, int y) {
- const WBASEEnhancementControl control = _wbaseEnhancements.controlAtPoint(x, y);
- if (control == kWBASEEnhancementControlNone)
- return false;
+void BaseGame::beginPointerControl(WBASEEnhancementControl control) {
_wbaseEnhancementHoveredControl = control;
- if (control == kWBASEEnhancementControlNavigationMap) {
- if (_wbaseEnhancementPanel == kWBASEEnhancementPanelNavigationMap)
- closeNavigationMap();
- else
- setEnhancementPanel(kWBASEEnhancementPanelNavigationMap);
- } else if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay) {
- if (!_wbaseEnhancements.forcedAutoplayEnabled() || _wbaseEnhancementsAutoplay.active())
- setEnhancementPanel(kWBASEEnhancementPanelNone);
+ if (!_wbaseEnhancements.controlEnabled(control, _wbaseEnhancementPanel)) {
+ _wbasePointerCapture = kPointerCaptureIgnored;
+ return;
+ }
+
+ uint32 action = 0;
+ switch (control) {
+ case kWBASEEnhancementControlForward:
+ action = kActionBaseForward;
+ break;
+ case kWBASEEnhancementControlBackward:
+ action = kActionBaseBackward;
+ break;
+ case kWBASEEnhancementControlTurnLeft:
+ action = kActionBaseTurnLeft;
+ break;
+ case kWBASEEnhancementControlTurnRight:
+ action = kActionBaseTurnRight;
+ break;
+ case kWBASEEnhancementControlFire:
+ action = kActionBaseFire;
+ break;
+ case kWBASEEnhancementControlExit:
+ cancelAutoplayForManualInput();
+ _wbasePointerCapture = kPointerCaptureControl;
+ _wbasePointerCapturedControl = control;
+ queueGameplayRequest(kPendingGameplayExit, kPendingGameplayPointer);
+ return;
+ case kWBASEEnhancementControlEscape:
+ action = kActionBaseMenu;
+ break;
+ case kWBASEEnhancementControlNavigationMap:
+ action = kActionWBASEEnhancementsNavigationMap;
+ break;
+ case kWBASEEnhancementControlAutoplay:
+ action = kActionWBASEEnhancementsAutoplay;
+ break;
+ default:
+ _wbasePointerCapture = kPointerCaptureIgnored;
+ return;
+ }
+
+ _wbasePointerCapture = kPointerCaptureControl;
+ _wbasePointerCapturedControl = control;
+ if (action >= kActionBaseForward && action <= kActionBaseFire) {
+ cancelAutoplayForManualInput();
+ setPointerGameplayAction(action, true);
} else {
- openAutoplayMenu();
+ handleAction(action, true);
}
- return true;
+}
+
+void BaseGame::releasePointerCapture() {
+ if (_wbasePointerCapture == kPointerCaptureControl) {
+ switch (_wbasePointerCapturedControl) {
+ case kWBASEEnhancementControlForward:
+ setPointerGameplayAction(kActionBaseForward, false);
+ break;
+ case kWBASEEnhancementControlBackward:
+ setPointerGameplayAction(kActionBaseBackward, false);
+ break;
+ case kWBASEEnhancementControlTurnLeft:
+ setPointerGameplayAction(kActionBaseTurnLeft, false);
+ break;
+ case kWBASEEnhancementControlTurnRight:
+ setPointerGameplayAction(kActionBaseTurnRight, false);
+ break;
+ case kWBASEEnhancementControlFire:
+ setPointerGameplayAction(kActionBaseFire, false);
+ break;
+ default:
+ break;
+ }
+ }
+ _wbasePointerButtonDown = false;
+ _wbasePointerCapture = kPointerCaptureNone;
+ _wbasePointerCapturedControl = kWBASEEnhancementControlNone;
+}
+
+void BaseGame::updatePointerCapture(WBASEEnhancementControl control) {
+ if (!_wbasePointerButtonDown || _wbasePointerCapture != kPointerCaptureControl ||
+ control == _wbasePointerCapturedControl)
+ return;
+
+ switch (_wbasePointerCapturedControl) {
+ case kWBASEEnhancementControlForward:
+ setPointerGameplayAction(kActionBaseForward, false, true);
+ break;
+ case kWBASEEnhancementControlBackward:
+ setPointerGameplayAction(kActionBaseBackward, false, true);
+ break;
+ case kWBASEEnhancementControlTurnLeft:
+ setPointerGameplayAction(kActionBaseTurnLeft, false, true);
+ break;
+ case kWBASEEnhancementControlTurnRight:
+ setPointerGameplayAction(kActionBaseTurnRight, false, true);
+ break;
+ case kWBASEEnhancementControlFire:
+ setPointerGameplayAction(kActionBaseFire, false, true);
+ break;
+ case kWBASEEnhancementControlExit:
+ cancelPointerGameplayRequests();
+ break;
+ default:
+ break;
+ }
+ _wbasePointerCapture = kPointerCaptureIgnored;
+ _wbasePointerCapturedControl = kWBASEEnhancementControlNone;
+}
+
+uint32 BaseGame::pressedEnhancementControls() const {
+ const uint32 active = _wbaseMappedInputHolds | _wbasePointerInputHolds;
+ uint32 controls = 0;
+ if (active & enhancementActionMask(kActionBaseForward))
+ controls |= 1U << kWBASEEnhancementControlForward;
+ if (active & enhancementActionMask(kActionBaseBackward))
+ controls |= 1U << kWBASEEnhancementControlBackward;
+ if (active & enhancementActionMask(kActionBaseTurnLeft))
+ controls |= 1U << kWBASEEnhancementControlTurnLeft;
+ if (active & enhancementActionMask(kActionBaseTurnRight))
+ controls |= 1U << kWBASEEnhancementControlTurnRight;
+ if (active & enhancementActionMask(kActionBaseFire))
+ controls |= 1U << kWBASEEnhancementControlFire;
+ if (_wbaseEnhancementHeldActions & enhancementActionMask(kActionBaseUse))
+ controls |= 1U << kWBASEEnhancementControlExit;
+ if (_wbaseEnhancementHeldActions & enhancementActionMask(kActionBaseMenu))
+ controls |= 1U << kWBASEEnhancementControlEscape;
+ if (_wbaseEnhancementHeldActions & enhancementActionMask(kActionWBASEEnhancementsNavigationMap))
+ controls |= 1U << kWBASEEnhancementControlNavigationMap;
+ if (_wbaseEnhancementHeldActions & enhancementActionMask(kActionWBASEEnhancementsAutoplay))
+ controls |= 1U << kWBASEEnhancementControlAutoplay;
+ if (_wbasePointerButtonDown && _wbasePointerCapture == kPointerCaptureControl)
+ controls |= 1U << _wbasePointerCapturedControl;
+ return controls;
}
bool BaseGame::enhancementPanelVisible() const {
@@ -736,26 +905,55 @@ void BaseGame::pollInput() {
case Common::EVENT_MOUSEMOVE:
if (_wbaseEnhancementInputArmed && _wbaseEnhancements.controlsEnabled()) {
Common::Point mouse;
- if (presentationToWBASEPoint(event.mouse, mouse))
+ if (presentationToWBASEPoint(event.mouse, mouse)) {
updateEnhancementPointer(mouse.x, mouse.y);
- else
+ updatePointerCapture(_wbaseEnhancements.controlAtPoint(mouse.x, mouse.y));
+ } else {
updateEnhancementPointer(-1, -1);
+ updatePointerCapture(kWBASEEnhancementControlNone);
+ }
}
break;
case Common::EVENT_LBUTTONDOWN: {
+ if (!_wbaseEnhancements.controlsEnabled() || _wbasePointerButtonDown)
+ break;
+ _wbasePointerButtonDown = true;
Common::Point mouse;
- if (!presentationToWBASEPoint(event.mouse, mouse))
+ if (!presentationToWBASEPoint(event.mouse, mouse)) {
+ _wbasePointerCapture = kPointerCaptureIgnored;
break;
- if (_wbaseEnhancementInputArmed && handleEnhancementControlClick(mouse.x, mouse.y))
+ }
+ const WBASEEnhancementControl control = _wbaseEnhancements.controlAtPoint(mouse.x, mouse.y);
+ updateEnhancementPointer(mouse.x, mouse.y);
+ if (!_wbaseEnhancementInputArmed) {
+ _wbasePointerCapture = kPointerCaptureIgnored;
break;
- if (_wbaseEnhancementInputArmed && _wbaseAutoplayMenuInputArmed &&
- _wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay) {
- if (!_wbaseEnhancementsAutoplay.selectMenuPointer(mouse.x, mouse.y))
+ }
+ if (enhancementPanelVisible()) {
+ if (control != kWBASEEnhancementControlNone) {
+ beginPointerControl(control);
break;
- startSelectedAutoplay();
+ }
+ if (_wbaseEnhancementPanel == kWBASEEnhancementPanelAutoplay) {
+ _wbasePointerCapture = kPointerCaptureChooser;
+ if (_wbaseAutoplayMenuInputArmed &&
+ _wbaseEnhancementsAutoplay.selectMenuPointer(mouse.x, mouse.y))
+ startSelectedAutoplay();
+ } else {
+ _wbasePointerCapture = kPointerCaptureIgnored;
+ }
+ break;
}
+ if (control != kWBASEEnhancementControlNone)
+ beginPointerControl(control);
+ else
+ _wbasePointerCapture = kPointerCaptureIgnored;
break;
}
+ case Common::EVENT_LBUTTONUP:
+ if (_wbaseEnhancements.controlsEnabled())
+ releasePointerCapture();
+ break;
default:
break;
}
@@ -860,8 +1058,7 @@ void BaseGame::handleAction(uint32 action, bool pressed) {
case kActionBaseTurnRight:
case kActionBaseFire:
case kActionBaseUse:
- _wbaseEnhancementsAutoplay.cancel();
- clearGameplayInput(false);
+ cancelAutoplayForManualInput();
break;
default:
break;
@@ -953,7 +1150,8 @@ void BaseGame::renderFrame() {
_wbaseEnhancements.forcedAutoplayEnabled());
}
_wbaseEnhancements.renderControls(_data, _wbaseEnhancementPanel,
- _wbaseEnhancementHoveredControl, _wbaseEnhancementsAutoplay.active(), _framebuffer.begin());
+ _wbaseEnhancementHoveredControl, pressedEnhancementControls(),
+ _wbaseEnhancementsAutoplay.active(), _framebuffer.begin());
Common::copy(_framebuffer.begin(), _framebuffer.end(), _vm->_graphicsMan->_frontBuffer);
_vm->_graphicsMan->addDirtyRect(0, 0, kBaseFrameWidth, kBaseFrameHeight);
updateEnhancementCursor();
diff --git a/engines/hopkins/base.h b/engines/hopkins/base.h
index c3c56892ebf..7025bcfc625 100644
--- a/engines/hopkins/base.h
+++ b/engines/hopkins/base.h
@@ -83,8 +83,19 @@ private:
enum PendingGameplayRequestType {
kPendingGameplayExit
};
+ enum PendingGameplayRequestSource {
+ kPendingGameplayMapped,
+ kPendingGameplayPointer
+ };
+ enum PointerCapture {
+ kPointerCaptureNone,
+ kPointerCaptureIgnored,
+ kPointerCaptureControl,
+ kPointerCaptureChooser
+ };
struct PendingGameplayRequest {
PendingGameplayRequestType type;
+ PendingGameplayRequestSource source;
uint32 generation;
};
@@ -100,10 +111,15 @@ private:
void releaseEnhancementCursor();
void updateEnhancementCursor();
void resetEnhancementSession();
+ void clearLogicalGameplayInput();
void clearGameplayInput(bool disarm);
BaseInputState consumeGameplayInput();
void setMappedGameplayAction(uint32 action, bool pressed);
- void queueGameplayRequest(PendingGameplayRequestType type);
+ void setPointerGameplayAction(uint32 action, bool pressed, bool cancelPulse = false);
+ void cancelAutoplayForManualInput();
+ void queueGameplayRequest(PendingGameplayRequestType type,
+ PendingGameplayRequestSource source = kPendingGameplayMapped);
+ void cancelPointerGameplayRequests();
bool updateEnhancementActionState(uint32 action, bool pressed);
void updateEnhancementInputArming(uint32 now);
void updateOverlayInputState();
@@ -112,7 +128,10 @@ private:
void closeNavigationMap();
void startSelectedAutoplay();
void updateEnhancementPointer(int x, int y);
- bool handleEnhancementControlClick(int x, int y);
+ void beginPointerControl(WBASEEnhancementControl control);
+ void releasePointerCapture();
+ void updatePointerCapture(WBASEEnhancementControl control);
+ uint32 pressedEnhancementControls() const;
bool enhancementPanelVisible() const;
void pollInput();
void handleAction(uint32 action, bool pressed);
@@ -131,6 +150,8 @@ private:
Common::Array<byte> _framebuffer;
uint32 _wbaseMappedInputHolds;
uint32 _wbaseMappedInputPulses;
+ uint32 _wbasePointerInputHolds;
+ uint32 _wbasePointerInputPulses;
Common::Queue<PendingGameplayRequest> _wbasePendingGameplayRequests;
uint32 _wbaseInputGeneration;
bool _wbaseTextureTogglePending;
@@ -157,6 +178,9 @@ private:
bool _wbaseEnhancementCursorPushed;
bool _wbaseEnhancementCursorVisible;
bool _wbaseOverlayVisible;
+ bool _wbasePointerButtonDown;
+ PointerCapture _wbasePointerCapture;
+ WBASEEnhancementControl _wbasePointerCapturedControl;
bool _quitRequested;
};
diff --git a/engines/hopkins/base_enhancements.cpp b/engines/hopkins/base_enhancements.cpp
index 387dfb3fb34..b263abd5817 100644
--- a/engines/hopkins/base_enhancements.cpp
+++ b/engines/hopkins/base_enhancements.cpp
@@ -38,14 +38,35 @@ namespace Hopkins {
namespace {
-static const int kMapButtonLeft = 244;
-static const int kMapButtonTop = 182;
-static const int kMapButtonRight = 279;
-static const int kMapButtonBottom = 197;
-static const int kAutoplayButtonLeft = 282;
-static const int kAutoplayButtonTop = 182;
-static const int kAutoplayButtonRight = 317;
-static const int kAutoplayButtonBottom = 197;
+struct EnhancementControlDefinition {
+ WBASEEnhancementControl control;
+ int left;
+ int top;
+ int right;
+ int bottom;
+};
+
+static const EnhancementControlDefinition kEnhancementControls[] = {
+ { kWBASEEnhancementControlForward, 2, 182, 26, 198 },
+ { kWBASEEnhancementControlBackward, 28, 182, 52, 198 },
+ { kWBASEEnhancementControlTurnLeft, 54, 182, 78, 198 },
+ { kWBASEEnhancementControlTurnRight, 80, 182, 104, 198 },
+ { kWBASEEnhancementControlFire, 110, 182, 146, 198 },
+ { kWBASEEnhancementControlExit, 150, 182, 186, 198 },
+ { kWBASEEnhancementControlEscape, 192, 182, 228, 198 },
+ { kWBASEEnhancementControlNavigationMap, 244, 182, 280, 198 },
+ { kWBASEEnhancementControlAutoplay, 282, 182, 318, 198 }
+};
+
+static const uint16 kTurnLeftGlyphRows[10] = {
+ 0x040, 0x080, 0x100, 0x3ff, 0x101,
+ 0x081, 0x041, 0x001, 0x001, 0x001
+};
+
+static const uint16 kTryExitGlyphRows[11] = {
+ 0x1fc0, 0x1040, 0x1048, 0x1044, 0x1002, 0x13ff,
+ 0x1002, 0x1044, 0x1048, 0x1040, 0x1fc0
+};
static byte nearestPaletteColor(const byte *palette, int red, int green, int blue) {
int bestDistance = 0x7fffffff;
@@ -69,6 +90,17 @@ static void drawPixel(byte *framebuffer, int x, int y, byte color) {
framebuffer[y * kBaseFrameWidth + x] = color;
}
+static void drawGlyphMask(byte *framebuffer, const uint16 *rows, int width, int height,
+ int x, int y, byte color, bool mirrorX) {
+ for (int row = 0; row < height; ++row) {
+ for (int column = 0; column < width; ++column) {
+ const int sourceColumn = mirrorX ? width - 1 - column : column;
+ if (rows[row] & (1U << (width - 1 - sourceColumn)))
+ drawPixel(framebuffer, x + column, y + row, color);
+ }
+ }
+}
+
static void drawLine(byte *framebuffer, int x0, int y0, int x1, int y1, byte color) {
const int deltaX = ABS(x1 - x0);
const int stepX = x0 < x1 ? 1 : -1;
@@ -153,6 +185,56 @@ static void drawAutoplayButtonGlyph(byte *framebuffer, int left, int top, byte c
fillRect(framebuffer, lockX - 1, lockY + 3, lockX + 5, lockY + 8, color);
}
+static void drawDirectionButtonGlyph(byte *framebuffer, const EnhancementControlDefinition &definition,
+ WBASEEnhancementControl control, byte color) {
+ const int centerX = (definition.left + definition.right - 1) / 2;
+ const int centerY = (definition.top + definition.bottom - 1) / 2;
+ if (control == kWBASEEnhancementControlForward || control == kWBASEEnhancementControlBackward) {
+ const int direction = control == kWBASEEnhancementControlForward ? -1 : 1;
+ drawLine(framebuffer, centerX, centerY - direction * 4,
+ centerX, centerY + direction * 4, color);
+ drawLine(framebuffer, centerX, centerY + direction * 4,
+ centerX - 3, centerY + direction, color);
+ drawLine(framebuffer, centerX, centerY + direction * 4,
+ centerX + 3, centerY + direction, color);
+ return;
+ }
+
+ drawGlyphMask(framebuffer, kTurnLeftGlyphRows, 10, 10,
+ definition.left + 7, definition.top + 3, color,
+ control == kWBASEEnhancementControlTurnRight);
+}
+
+static void drawFireButtonGlyph(byte *framebuffer, const EnhancementControlDefinition &definition,
+ byte color) {
+ const int centerX = (definition.left + definition.right - 1) / 2;
+ const int centerY = (definition.top + definition.bottom - 1) / 2;
+ drawLine(framebuffer, centerX - 5, centerY, centerX + 5, centerY, color);
+ drawLine(framebuffer, centerX, centerY - 5, centerX, centerY + 5, color);
+ drawLine(framebuffer, centerX - 3, centerY - 3, centerX + 3, centerY + 3, color);
+ drawLine(framebuffer, centerX - 3, centerY + 3, centerX + 3, centerY - 3, color);
+}
+
+static void drawExitButtonGlyph(Graphics::Surface &surface, byte *framebuffer,
+ const EnhancementControlDefinition &definition, const Graphics::Font &font, byte color) {
+ const Common::String label("Try");
+ const int gap = 2;
+ const int glyphWidth = 13;
+ const int glyphHeight = 11;
+ const int buttonWidth = definition.right - definition.left;
+ const int buttonHeight = definition.bottom - definition.top;
+ const int textWidth = font.getStringWidth(label);
+ const int groupWidth = textWidth + gap + glyphWidth;
+ const int groupLeft = definition.left + (buttonWidth - groupWidth) / 2;
+ const int textTop = definition.top + (buttonHeight - font.getFontHeight()) / 2;
+ const int glyphLeft = groupLeft + textWidth + gap;
+ const int glyphTop = definition.top + (buttonHeight - glyphHeight) / 2;
+
+ font.drawString(&surface, label, groupLeft, textTop, textWidth, color);
+ drawGlyphMask(framebuffer, kTryExitGlyphRows, glyphWidth, glyphHeight,
+ glyphLeft, glyphTop, color, false);
+}
+
static void drawPlayerArrow(const BaseData &data, byte *framebuffer, int centerX, int centerY,
int angle, byte fillColor, byte outlineColor) {
const int32 directionX = data.cosQ16(angle);
@@ -210,7 +292,8 @@ static void drawGuardMarker(byte *framebuffer, int centerX, int centerY, byte co
}
static void drawOpaqueLegend(const BaseData &data, byte *framebuffer, byte backgroundColor,
- byte textColor, byte playerColor, byte exitColor, byte guardColor, byte doorColor) {
+ byte textColor, byte playerColor, byte exitColor, byte guardColor, byte doorColor,
+ bool forcedAutoplay) {
Graphics::Surface surface;
surface.init(kBaseFrameWidth, kBaseFrameHeight, kBaseFrameWidth, framebuffer,
Graphics::PixelFormat::createFormatCLUT8());
@@ -239,9 +322,9 @@ static void drawOpaqueLegend(const BaseData &data, byte *framebuffer, byte backg
font->drawString(&surface, WBASEEnhancementsAutoplay::destinationMapLabel(index),
243, 51 + index * 15, 75, textColor);
}
- font->drawString(&surface, _("A: autoplay"), 226, 151, 92, textColor,
- Graphics::kTextAlignCenter);
- font->drawString(&surface, _("M/Esc: close map"), 76, 183, 160, textColor,
+ const Common::String footer = forcedAutoplay ? _("M: close A: autoplay Esc: menu") :
+ _("M/Esc: close A: autoplay");
+ font->drawString(&surface, footer, 0, 169, kBaseFrameWidth, textColor,
Graphics::kTextAlignCenter);
}
@@ -299,7 +382,7 @@ static void drawMapMarkers(const BaseData &data, const BaseEngine &engine, byte
}
static void renderNavigationMapOpaque(const BaseData &data, const BaseEngine &engine,
- byte *framebuffer, const NavigationMapColors &colors) {
+ byte *framebuffer, const NavigationMapColors &colors, bool forcedAutoplay) {
Common::fill(framebuffer, framebuffer + kBaseFrameWidth * kBaseFrameHeight, colors.background);
static const int kMapScale = 2;
@@ -322,7 +405,7 @@ static void renderNavigationMapOpaque(const BaseData &data, const BaseEngine &en
drawMapMarkers(data, engine, framebuffer, kMapScale, mapLeft, mapTop, colors);
drawOpaqueLegend(data, framebuffer, colors.background, colors.highlight, colors.player,
- colors.exit, colors.guard, colors.door);
+ colors.exit, colors.guard, colors.door, forcedAutoplay);
}
} // End of anonymous namespace
@@ -337,23 +420,35 @@ void WBASEEnhancements::renderNavigationMap(const BaseData &data, const BaseEngi
return;
const NavigationMapColors colors = navigationMapColors(data);
- renderNavigationMapOpaque(data, engine, framebuffer, colors);
+ renderNavigationMapOpaque(data, engine, framebuffer, colors, _forcedAutoplay);
}
WBASEEnhancementControl WBASEEnhancements::controlAtPoint(int x, int y) const {
if (!controlsEnabled())
return kWBASEEnhancementControlNone;
- if (x >= kMapButtonLeft && x <= kMapButtonRight &&
- y >= kMapButtonTop && y <= kMapButtonBottom)
- return kWBASEEnhancementControlNavigationMap;
- if (x >= kAutoplayButtonLeft && x <= kAutoplayButtonRight &&
- y >= kAutoplayButtonTop && y <= kAutoplayButtonBottom)
- return kWBASEEnhancementControlAutoplay;
+ for (uint i = 0; i < ARRAYSIZE(kEnhancementControls); ++i) {
+ const EnhancementControlDefinition &definition = kEnhancementControls[i];
+ if (x >= definition.left && x < definition.right &&
+ y >= definition.top && y < definition.bottom)
+ return definition.control;
+ }
return kWBASEEnhancementControlNone;
}
+bool WBASEEnhancements::controlEnabled(WBASEEnhancementControl control,
+ WBASEEnhancementPanel panel) const {
+ if (!controlsEnabled() || control == kWBASEEnhancementControlNone)
+ return false;
+ if (control == kWBASEEnhancementControlNavigationMap ||
+ control == kWBASEEnhancementControlAutoplay ||
+ control == kWBASEEnhancementControlEscape)
+ return true;
+ return panel == kWBASEEnhancementPanelNone && !_forcedAutoplay;
+}
+
void WBASEEnhancements::renderControls(const BaseData &data, WBASEEnhancementPanel panel,
- WBASEEnhancementControl hoveredControl, bool autoplayActive, byte *framebuffer) const {
+ WBASEEnhancementControl hoveredControl, uint32 pressedControls,
+ bool autoplayActive, byte *framebuffer) const {
if (!controlsEnabled() || !framebuffer)
return;
@@ -368,30 +463,63 @@ void WBASEEnhancements::renderControls(const BaseData &data, WBASEEnhancementPan
const byte background = nearestPaletteColor(palette, 0, 0, 0);
const byte idle = nearestPaletteColor(palette, 105, 105, 105);
const byte hover = nearestPaletteColor(palette, 255, 255, 255);
+ const byte pressed = nearestPaletteColor(palette, 255, 140, 0);
+ const byte disabled = nearestPaletteColor(palette, 55, 55, 55);
const byte mapActive = nearestPaletteColor(palette, 0, 220, 255);
const byte chooserActive = nearestPaletteColor(palette, 255, 220, 0);
const byte autoplayRunning = nearestPaletteColor(palette, 40, 255, 60);
- const byte mapColor = panel == kWBASEEnhancementPanelNavigationMap ? mapActive : idle;
- const byte autoplayColor = panel == kWBASEEnhancementPanelAutoplay ? chooserActive :
- (autoplayActive ? autoplayRunning : idle);
- const byte mapBorder = hoveredControl == kWBASEEnhancementControlNavigationMap ? hover : mapColor;
- const byte autoplayBorder = hoveredControl == kWBASEEnhancementControlAutoplay ? hover : autoplayColor;
-
- fillRect(framebuffer, kMapButtonLeft, kMapButtonTop, kMapButtonRight, kMapButtonBottom, background);
- drawFrame(framebuffer, kMapButtonLeft, kMapButtonTop, kMapButtonRight, kMapButtonBottom, mapBorder);
- drawMapButtonGlyph(framebuffer, kMapButtonLeft, kMapButtonTop, mapColor);
- font->drawString(&surface, Common::String("M"), kMapButtonLeft + 22, kMapButtonTop + 4, 9, mapColor,
- Graphics::kTextAlignCenter);
-
- fillRect(framebuffer, kAutoplayButtonLeft, kAutoplayButtonTop,
- kAutoplayButtonRight, kAutoplayButtonBottom, background);
- drawFrame(framebuffer, kAutoplayButtonLeft, kAutoplayButtonTop,
- kAutoplayButtonRight, kAutoplayButtonBottom, autoplayBorder);
- drawAutoplayButtonGlyph(framebuffer, kAutoplayButtonLeft, kAutoplayButtonTop,
- autoplayColor, _forcedAutoplay);
- font->drawString(&surface, Common::String("A"), kAutoplayButtonLeft + 15, kAutoplayButtonTop + 4, 9,
- autoplayColor, Graphics::kTextAlignCenter);
+ for (uint i = 0; i < ARRAYSIZE(kEnhancementControls); ++i) {
+ const EnhancementControlDefinition &definition = kEnhancementControls[i];
+ const bool enabled = controlEnabled(definition.control, panel);
+ byte color = enabled ? idle : disabled;
+ if (definition.control == kWBASEEnhancementControlNavigationMap &&
+ panel == kWBASEEnhancementPanelNavigationMap)
+ color = mapActive;
+ else if (definition.control == kWBASEEnhancementControlAutoplay)
+ color = panel == kWBASEEnhancementPanelAutoplay ? chooserActive :
+ (autoplayActive ? autoplayRunning : color);
+ if (enabled && (pressedControls & (1U << definition.control)))
+ color = pressed;
+ const byte border = enabled && hoveredControl == definition.control ? hover : color;
+
+ fillRect(framebuffer, definition.left, definition.top,
+ definition.right - 1, definition.bottom - 1, background);
+ drawFrame(framebuffer, definition.left, definition.top,
+ definition.right - 1, definition.bottom - 1, border);
+ switch (definition.control) {
+ case kWBASEEnhancementControlForward:
+ case kWBASEEnhancementControlBackward:
+ case kWBASEEnhancementControlTurnLeft:
+ case kWBASEEnhancementControlTurnRight:
+ drawDirectionButtonGlyph(framebuffer, definition, definition.control, color);
+ break;
+ case kWBASEEnhancementControlFire:
+ drawFireButtonGlyph(framebuffer, definition, color);
+ break;
+ case kWBASEEnhancementControlExit:
+ drawExitButtonGlyph(surface, framebuffer, definition, *font, color);
+ break;
+ case kWBASEEnhancementControlEscape:
+ font->drawString(&surface, Common::String("Esc"), definition.left + 2,
+ definition.top + 4, definition.right - definition.left - 4, color,
+ Graphics::kTextAlignCenter);
+ break;
+ case kWBASEEnhancementControlNavigationMap:
+ drawMapButtonGlyph(framebuffer, definition.left, definition.top, color);
+ font->drawString(&surface, Common::String("M"), definition.left + 22,
+ definition.top + 4, 9, color, Graphics::kTextAlignCenter);
+ break;
+ case kWBASEEnhancementControlAutoplay:
+ drawAutoplayButtonGlyph(framebuffer, definition.left, definition.top,
+ color, _forcedAutoplay);
+ font->drawString(&surface, Common::String("A"), definition.left + 15,
+ definition.top + 4, 9, color, Graphics::kTextAlignCenter);
+ break;
+ default:
+ break;
+ }
+ }
}
} // End of namespace Hopkins
diff --git a/engines/hopkins/base_enhancements.h b/engines/hopkins/base_enhancements.h
index 0c433339333..aef5fcfc467 100644
--- a/engines/hopkins/base_enhancements.h
+++ b/engines/hopkins/base_enhancements.h
@@ -43,7 +43,14 @@ enum WBASEEnhancementPanel {
enum WBASEEnhancementControl {
kWBASEEnhancementControlNone,
kWBASEEnhancementControlNavigationMap,
- kWBASEEnhancementControlAutoplay
+ kWBASEEnhancementControlAutoplay,
+ kWBASEEnhancementControlForward,
+ kWBASEEnhancementControlBackward,
+ kWBASEEnhancementControlTurnLeft,
+ kWBASEEnhancementControlTurnRight,
+ kWBASEEnhancementControlFire,
+ kWBASEEnhancementControlExit,
+ kWBASEEnhancementControlEscape
};
class WBASEEnhancements {
@@ -56,8 +63,10 @@ public:
bool navigationMapEnabled() const { return controlsEnabled(); }
void renderNavigationMap(const BaseData &data, const BaseEngine &engine, byte *framebuffer) const;
WBASEEnhancementControl controlAtPoint(int x, int y) const;
+ bool controlEnabled(WBASEEnhancementControl control, WBASEEnhancementPanel panel) const;
void renderControls(const BaseData &data, WBASEEnhancementPanel panel,
- WBASEEnhancementControl hoveredControl, bool autoplayActive, byte *framebuffer) const;
+ WBASEEnhancementControl hoveredControl, uint32 pressedControls,
+ bool autoplayActive, byte *framebuffer) const;
private:
bool _enabled;
Commit: 0ecd32937639db72c4c0d3881b7a412c6220d869
https://github.com/scummvm/scummvm/commit/0ecd32937639db72c4c0d3881b7a412c6220d869
Author: fusefib (fibofuse at gmail.com)
Date: 2026-10-09T21:47:54-10:00
Commit Message:
HOPKINS: WBASE: Add viewport exit-or-fire input
Exit detection takes priority so using a valid doorway cannot also fire
the weapon.
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/hopkins/base.cpp
engines/hopkins/base.h
engines/hopkins/base_engine.cpp
engines/hopkins/base_engine.h
engines/hopkins/metaengine.cpp
diff --git a/engines/hopkins/base.cpp b/engines/hopkins/base.cpp
index 91a08aaf827..1efe08d47dc 100644
--- a/engines/hopkins/base.cpp
+++ b/engines/hopkins/base.cpp
@@ -560,6 +560,8 @@ BaseInputState BaseGame::consumeGameplayInput() {
const PendingGameplayRequest request = _wbasePendingGameplayRequests.pop();
if (request.type == kPendingGameplayExit)
input.exitRequested = true;
+ else if (request.type == kPendingGameplayContext)
+ input.contextRequested = true;
}
return input;
}
@@ -946,8 +948,13 @@ void BaseGame::pollInput() {
}
if (control != kWBASEEnhancementControlNone)
beginPointerControl(control);
- else
+ else if (mouse.y < kBaseViewHeight && !_wbaseEnhancements.forcedAutoplayEnabled()) {
+ cancelAutoplayForManualInput();
+ _wbasePointerCapture = kPointerCaptureWorld;
+ queueGameplayRequest(kPendingGameplayContext, kPendingGameplayPointer);
+ } else {
_wbasePointerCapture = kPointerCaptureIgnored;
+ }
break;
}
case Common::EVENT_LBUTTONUP:
diff --git a/engines/hopkins/base.h b/engines/hopkins/base.h
index 7025bcfc625..041bb64e00f 100644
--- a/engines/hopkins/base.h
+++ b/engines/hopkins/base.h
@@ -81,7 +81,8 @@ private:
class SessionGuard;
enum PendingGameplayRequestType {
- kPendingGameplayExit
+ kPendingGameplayExit,
+ kPendingGameplayContext
};
enum PendingGameplayRequestSource {
kPendingGameplayMapped,
@@ -91,7 +92,8 @@ private:
kPointerCaptureNone,
kPointerCaptureIgnored,
kPointerCaptureControl,
- kPointerCaptureChooser
+ kPointerCaptureChooser,
+ kPointerCaptureWorld
};
struct PendingGameplayRequest {
PendingGameplayRequestType type;
diff --git a/engines/hopkins/base_engine.cpp b/engines/hopkins/base_engine.cpp
index aa4fef81c7a..8b81b42e87a 100644
--- a/engines/hopkins/base_engine.cpp
+++ b/engines/hopkins/base_engine.cpp
@@ -193,13 +193,27 @@ int BaseEngine::tick(BaseInputState &input) {
updateTurn(input);
updatePlayer(input);
- updateWeapon(input);
int exitResult = -1;
- if (input.exitRequested) {
+ bool exitSoundQueued = false;
+ BaseInputState weaponInput = input;
+ if (input.contextRequested) {
exitResult = tryExit();
- input.exitRequested = false;
+ input.contextRequested = false;
if (exitResult >= 94 && exitResult <= 99)
+ exitSoundQueued = true;
+ else
+ weaponInput.fire = true;
+ }
+ updateWeapon(weaponInput);
+ if (exitSoundQueued)
+ _soundEvents.push_back(kBaseSoundDoorOrExit);
+
+ if (input.exitRequested) {
+ if (exitResult < 0)
+ exitResult = tryExit();
+ input.exitRequested = false;
+ if (exitResult >= 94 && exitResult <= 99 && !exitSoundQueued)
_soundEvents.push_back(kBaseSoundDoorOrExit);
}
diff --git a/engines/hopkins/base_engine.h b/engines/hopkins/base_engine.h
index 77e895bd9ae..8caba182c0f 100644
--- a/engines/hopkins/base_engine.h
+++ b/engines/hopkins/base_engine.h
@@ -39,10 +39,12 @@ struct BaseInputState {
bool turnRight;
bool fire;
bool exitRequested;
+ bool contextRequested;
bool toggleTextures;
BaseInputState() : forward(false), backward(false), turnLeft(false),
- turnRight(false), fire(false), exitRequested(false), toggleTextures(false) {}
+ turnRight(false), fire(false), exitRequested(false), contextRequested(false),
+ toggleTextures(false) {}
};
enum BaseSoundEvent {
diff --git a/engines/hopkins/metaengine.cpp b/engines/hopkins/metaengine.cpp
index d4d1e455b79..e380871a1b4 100644
--- a/engines/hopkins/metaengine.cpp
+++ b/engines/hopkins/metaengine.cpp
@@ -69,7 +69,7 @@ static const ADExtraGuiOptionsMap optionsList[] = {
{
GAMEOPTION_WBASE_ENHANCEMENTS,
{
- _s("WBASE enhancements"),
+ _s("Underwater base shooter enhancements"),
_s("Enable navigation map and autoplay options for Windows base shooter"),
kWBASEEnhancementsConfigKey,
false,
@@ -80,7 +80,7 @@ static const ADExtraGuiOptionsMap optionsList[] = {
{
GAMEOPTION_WBASE_FORCED_AUTOPLAY,
{
- _s("WBASE Forced Autoplay"),
+ _s("Underwater base shooter Forced Autoplay"),
_s("Require an autoplay destination in Windows base shooter"),
kWBASEForcedAutoplayConfigKey,
false,
More information about the Scummvm-git-logs
mailing list