[Scummvm-git-logs] scummvm master -> 461eeec147ba2265a1eb8874817d88a721a32759

Helco noreply at scummvm.org
Sun Feb 8 18:16:20 UTC 2026


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

Summary:
c73f1744e2 ALCACHOFA: Reduce public API of Camera
1cbaff76f4 ALCACHOFA: Split Camera into base class and V3
8ade46170b ALCACHOFA: TERROR: Fix outro
461eeec147 ALCACHOFA: Add CameraV1


Commit: c73f1744e2fe7c20990799f0877054887daa03df
    https://github.com/scummvm/scummvm/commit/c73f1744e2fe7c20990799f0877054887daa03df
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-08T19:01:39+01:00

Commit Message:
ALCACHOFA: Reduce public API of Camera

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/menu.cpp
    engines/alcachofa/player.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 530b908c736..2206e8ad439 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -113,7 +113,7 @@ Common::Error AlcachofaEngine::run() {
 		_sounds.update();
 		_renderer->begin();
 		_drawQueue->clear();
-		_camera.shake() = Vector2d();
+		_camera.preUpdate();
 		_player->preUpdate();
 		if (_player->currentRoom() != nullptr)
 			_player->currentRoom()->update();
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 00f2530dfcd..da2e5831f31 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -21,7 +21,7 @@
 
 #include "alcachofa/camera.h"
 #include "alcachofa/alcachofa.h"
-#include "alcachofa/script.h"
+#include "alcachofa/graphics.h"
 
 #include "common/system.h"
 #include "math/vector4d.h"
@@ -31,28 +31,35 @@ using namespace Math;
 
 namespace Alcachofa {
 
-void Camera::resetRotationAndScale() {
-	_cur._scale = 1;
-	_cur._rotation = 0;
-	_cur._usedCenter.z() = 0;
+void Camera::preUpdate() {
+	_shake = {};
 }
 
-void Camera::setRoomBounds(Point min, Point size) {
-	Point screenSize(g_system->getWidth(), g_system->getHeight());
-	_roomMin = as2D(min + screenSize / 2);
-	_roomMax = _roomMin + as2D(size - screenSize);
-	_roomScale = 0;
-}
-
-void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
-	float scaleFactor = 1 - bgScale * kInvBaseScale;
-	_roomMin = Vector2d(
-		g_system->getWidth() / 2 * scaleFactor,
-		g_system->getHeight() / 2 * scaleFactor);
-	_roomMax = _roomMin + Vector2d(
-		bgSize.x * bgScale * kInvBaseScale,
-		bgSize.y * bgScale * kInvBaseScale);
-	_roomScale = bgScale;
+void Camera::setRoomBounds(Graphic &background) {
+	auto bgSize = background.animation().imageSize(0);
+	if (g_engine->isV1()) {
+		Point screenSize(g_system->getWidth(), g_system->getHeight());
+		_roomMin = as2D(background.topLeft() + screenSize / 2);
+		_roomMax = _roomMin + as2D(bgSize - screenSize);
+		_roomScale = 0;
+	} else {
+		/* The fallback fixes a bug where if the background image is invalid the original engine
+		 * would not update the background size. This would be around 1024,768 due to
+		 * previous rooms in the bug instances I found.
+		 */
+		if (bgSize == Point(0, 0))
+			bgSize = Point(1024, 768);
+
+		const auto bgScale = background.scale();
+		float scaleFactor = 1 - bgScale * kInvBaseScale;
+		_roomMin = Vector2d(
+			g_system->getWidth() / 2 * scaleFactor,
+			g_system->getHeight() / 2 * scaleFactor);
+		_roomMax = _roomMin + Vector2d(
+			bgSize.x * bgScale * kInvBaseScale,
+			bgSize.y * bgScale * kInvBaseScale);
+		_roomScale = bgScale;
+	}
 }
 
 void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
@@ -64,6 +71,49 @@ void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
 		_isChanging = false;
 }
 
+void Camera::onChangedRoom(bool resetCamera) {
+	if (resetCamera)
+		resetRotationAndScale();
+	if (_followTarget != nullptr)
+		setFollow(_followTarget, true);
+}
+
+void Camera::onTriggeredDoor(WalkingCharacter *target) {
+	setFollow(target, true);
+}
+
+void Camera::onTriggeredDoor(Point fixedPosition) {
+	setPosition(as2D(fixedPosition));
+}
+
+void Camera::onScriptChangedCharacter(MainCharacterKind kind) {
+	resetRotationAndScale();
+	if (kind != MainCharacterKind::None)
+		setFollow(g_engine->player().activeCharacter());
+	backup(0);
+}
+
+void Camera::onUserChangedCharacter() {
+	setFollow(g_engine->player().activeCharacter());
+	restore(0);
+}
+
+void Camera::onOpenMenu() {
+	backup(1);
+	setPosition(Math::Vector3d(
+		g_system->getWidth() / 2.0f, g_system->getHeight() / 2.0f, 0.0f));
+}
+
+void Camera::onCloseMenu() {
+	restore(1);
+}
+
+void Camera::resetRotationAndScale() {
+	_cur._scale = 1;
+	_cur._rotation = 0;
+	_cur._usedCenter.z() = 0;
+}
+
 void Camera::setPosition(Vector2d v) {
 	setPosition({ v.getX(), v.getY(), _cur._usedCenter.z() });
 }
@@ -487,7 +537,7 @@ protected:
 	void update(float t) override {
 		const Vector2d phase = _frequency * t * (float)M_PI * 2.0f;
 		const float amplTimeFactor = 1.0f / expf(t * 5.0f); // a curve starting at 1, depreciating towards 0 
-		_camera.shake() = {
+		_camera._shake = {
 			sinf(phase.getX()) * _amplitude.getX() * amplTimeFactor,
 			sinf(phase.getY()) * _amplitude.getY() * amplTimeFactor
 		};
@@ -692,7 +742,7 @@ struct CamDisguiseTask final : public Task {
 
 	TaskReturn run() override {
 		if (_startTime == 0) {
-			_startPosition = { _camera.usedCenter().x(), _camera.usedCenter().y() };
+			_startPosition = { _camera._cur._usedCenter.x(), _camera._cur._usedCenter.y() };
 			_startTime = g_engine->getMillis();
 		}
 		if (_durationMs <= 0 || g_engine->getMillis() - _startTime >= (uint32)_durationMs)
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index c6774da47bf..e7424587f51 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -27,6 +27,7 @@
 
 namespace Alcachofa {
 
+class Graphic;
 class WalkingCharacter;
 class Process;
 struct Task;
@@ -36,24 +37,24 @@ static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
 
 class Camera {
 public:
-	inline Math::Vector3d usedCenter() const { return _cur._usedCenter; }
 	inline Math::Angle rotation() const { return _cur._rotation; }
-	inline Math::Vector2d &shake() { return _shake; }
-	inline WalkingCharacter *followTarget() { return _followTarget; }
 
+	void preUpdate();
 	void update();
+	void setRoomBounds(Graphic &background);
+	void setFollow(WalkingCharacter *target, bool catchUp = false);
+	void onChangedRoom(bool resetCamera);
+	void onTriggeredDoor(WalkingCharacter *target);
+	void onTriggeredDoor(Common::Point fixedPosition);
+	void onScriptChangedCharacter(MainCharacterKind kind);
+	void onUserChangedCharacter();
+	void onOpenMenu();
+	void onCloseMenu();
+	void syncGame(Common::Serializer &s);
+
 	Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
 	Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
 	Common::Point transform3Dto2D(Common::Point p) const;
-	void resetRotationAndScale();
-	void setRoomBounds(Common::Point min, Common::Point size); ///< Used in V1
-	void setRoomBounds(Common::Point bgSize, int16 bgScale); ///< Used in V3
-	void setFollow(WalkingCharacter *target, bool catchUp = false);
-	void setPosition(Math::Vector2d v);
-	void setPosition(Math::Vector3d v);
-	void backup(uint slot);
-	void restore(uint slot);
-	void syncGame(Common::Serializer &s);
 
 	Task *lerpPos(Process &process,
 		Math::Vector2d targetPos,
@@ -78,6 +79,12 @@ public:
 	Task *disguise(Process &process, int32 duration);
 
 private:
+	void resetRotationAndScale();
+	void setPosition(Math::Vector2d v);
+	void setPosition(Math::Vector3d v);
+	void backup(uint slot);
+	void restore(uint slot);
+
 	friend struct CamLerpTask;
 	friend struct CamLerpPosTask;
 	friend struct CamLerpScaleTask;
@@ -86,6 +93,7 @@ private:
 	friend struct CamShakeTask;
 	friend struct CamWaitToStopTask;
 	friend struct CamSetInactiveAttributeTask;
+	friend struct CamDisguiseTask;
 	Math::Vector3d setAppliedCenter(Math::Vector3d center);
 	void setupMatricesAround(Math::Vector3d center);
 	void updateFollowing(float deltaTime);
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 044ee034d62..1c9d8aedd26 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -143,8 +143,7 @@ bool GlobalUI::updateChangingCharacter() {
 
 	player.setActiveCharacter(player.inactiveCharacter()->kind());
 	player.heldItem() = nullptr;
-	g_engine->camera().setFollow(player.activeCharacter());
-	g_engine->camera().restore(0);
+	g_engine->camera().onUserChangedCharacter();
 	player.changeRoom(player.activeCharacter()->room()->name(), false);
 	g_engine->game().onUserChangedCharacter();
 
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 5576f6fac1d..a211ffdad47 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -100,9 +100,7 @@ void Menu::updateOpeningMenu() {
 
 	g_engine->player().heldItem() = nullptr;
 	g_engine->scheduler().backupContext();
-	g_engine->camera().backup(1);
-	g_engine->camera().setPosition(Math::Vector3d(
-		g_system->getWidth() / 2.0f, g_system->getHeight() / 2.0f, 0.0f));
+	g_engine->camera().onOpenMenu();
 }
 
 void MenuV1::updateOpeningMenu() {
@@ -193,7 +191,7 @@ void Menu::continueGame() {
 	g_engine->input().nextFrame(); // presumably to clear all was* flags
 	g_engine->player().changeRoom(_previousRoom->name(), true);
 	g_engine->sounds().pauseAll(false);
-	g_engine->camera().restore(1);
+	g_engine->camera().onCloseMenu();
 	g_engine->scheduler().restoreContext();
 	g_engine->setMillis(_millisBeforeMenu);
 }
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 1f19b7e3070..e4cdc817d84 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -162,11 +162,7 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera,
 	}
 	_currentRoom->loadResources(); // if we kept resources we loop over a couple noops, that is fine.
 
-	if (resetCamera)
-		g_engine->camera().resetRotationAndScale();
-	WalkingCharacter *followTarget = g_engine->camera().followTarget();
-	if (followTarget != nullptr)
-		g_engine->camera().setFollow(followTarget, true);
+	g_engine->camera().onChangedRoom(resetCamera);
 	_pressedObject = _selectedObject = nullptr;
 }
 
@@ -257,12 +253,12 @@ struct DoorTask final : public Task {
 		_player.changeRoom(_targetRoom->name(), true); //-V779
 
 		if (_targetRoom->fixedCameraOnEntering())
-			g_engine->camera().setPosition(as2D(_targetObject->interactionPoint()));
+			g_engine->camera().onTriggeredDoor(_targetObject->interactionPoint());
 		else {
 			_character->room() = _targetRoom;
 			_character->setPosition(_targetObject->interactionPoint());
 			_character->stopWalking(_targetDirection);
-			g_engine->camera().setFollow(_character, true);
+			g_engine->camera().onTriggeredDoor(_character);
 		}
 
 		g_engine->sounds().setMusicToRoom(_targetRoom->musicID());
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 4de14e1ce0f..7dbba1a9f1c 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -272,20 +272,8 @@ void Room::updateInteraction() {
 
 void Room::updateRoomBounds() {
 	auto graphic = _backgroundObject == nullptr ? nullptr : _backgroundObject->graphic();
-	if (graphic != nullptr) {
-		auto bgSize = graphic->animation().imageSize(0);
-		if (g_engine->isV1()) {
-			g_engine->camera().setRoomBounds(graphic->topLeft(), bgSize);
-		} else {
-			/* The fallback fixes a bug where if the background image is invalid the original engine
-			 * would not update the background size. This would be around 1024,768 due to
-			 * previous rooms in the bug instances I found.
-			 */
-			if (bgSize == Point(0, 0))
-				bgSize = Point(1024, 768);
-			g_engine->camera().setRoomBounds(bgSize, graphic->scale());
-		}
-	}
+	if (graphic != nullptr)
+		g_engine->camera().setRoomBounds(*graphic);
 }
 
 void Room::updateObjects() {
@@ -533,13 +521,13 @@ void Inventory::drawAsOverlay(int32 scrollY) {
 }
 
 void Inventory::open() {
-	g_engine->camera().backup(1);
+	g_engine->camera().onOpenMenu();
 	g_engine->player().changeRoom(name(), true);
 	updateItemsByActiveCharacter();
 }
 
 void Inventory::close() {
-	g_engine->camera().restore(1);
+	g_engine->camera().onCloseMenu();
 	g_engine->globalUI().startClosingInventory();
 }
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 99dfe140878..578ea0f23cb 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -702,16 +702,12 @@ private:
 		// player/world state changes
 		case ScriptKernelTask::ChangeCharacter: {
 			MainCharacterKind kind = getMainCharacterKindArg(0);
-			auto &camera = g_engine->camera();
 			auto &player = g_engine->player();
-			camera.resetRotationAndScale();
-			camera.backup(0);
 			if (kind != MainCharacterKind::None) {
 				player.setActiveCharacter(kind);
 				player.heldItem() = nullptr;
-				camera.setFollow(player.activeCharacter());
-				camera.backup(0);
 			}
+			g_engine->camera().onScriptChangedCharacter(kind);
 
 			if (g_engine->game().shouldChangeCharacterUseGameLock()) {
 				killProcessesFor(MainCharacterKind::None); // yes, kill for all characters


Commit: 1cbaff76f400e24b12ef6b5482119df8b2c3c509
    https://github.com/scummvm/scummvm/commit/1cbaff76f400e24b12ef6b5482119df8b2c3c509
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-08T19:01:39+01:00

Commit Message:
ALCACHOFA: Split Camera into base class and V3

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 2206e8ad439..a921d76c421 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -84,6 +84,7 @@ Common::Error AlcachofaEngine::run() {
 	_world.load();
 	_renderer.reset(IRenderer::createOpenGLRenderer(game().getResolution()));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
+	_camera.reset(new CameraV3());
 	_script.reset(new Script());
 	_player.reset(new Player());
 	_globalUI.reset(isV1() ? static_cast<GlobalUI *>(new GlobalUIV1()) : new GlobalUIV3());
@@ -113,7 +114,7 @@ Common::Error AlcachofaEngine::run() {
 		_sounds.update();
 		_renderer->begin();
 		_drawQueue->clear();
-		_camera.preUpdate();
+		_camera->preUpdate();
 		_player->preUpdate();
 		if (_player->currentRoom() != nullptr)
 			_player->currentRoom()->update();
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 45b2ca5b27f..a75acdf8673 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -119,7 +119,7 @@ public:
 	inline const AlcachofaGameDescription &gameDescription() const { return *_gameDescription; }
 	inline IRenderer &renderer() { return *_renderer; }
 	inline DrawQueue &drawQueue() { return *_drawQueue; }
-	inline Camera &camera() { return _camera; }
+	inline Camera &camera() { return *_camera; }
 	inline Input &input() { return _input; }
 	inline Sounds &sounds() { return _sounds; }
 	inline Player &player() { return *_player; }
@@ -133,6 +133,12 @@ public:
 	inline Config &config() { return _config; }
 	inline bool isDebugModeActive() const { return _debugHandler != nullptr; }
 
+	inline CameraV3 &cameraV3() {
+		auto result = dynamic_cast<CameraV3 *>(_camera.get());
+		scumm_assert(result != nullptr);
+		return *result;
+	}
+
 	uint32 getMillis() const;
 	void setMillis(uint32 newMillis);
 	void pauseEngineIntern(bool pause) override;
@@ -183,8 +189,8 @@ private:
 	Common::ScopedPtr<GlobalUI> _globalUI;
 	Common::ScopedPtr<Menu> _menu;
 	Common::ScopedPtr<Game> _game;
+	Common::ScopedPtr<Camera> _camera;
 	World _world;
-	Camera _camera;
 	Input _input;
 	Sounds _sounds;
 	Scheduler _scheduler;
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index da2e5831f31..46db22db5ba 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -31,11 +31,110 @@ using namespace Math;
 
 namespace Alcachofa {
 
-void Camera::preUpdate() {
+//
+// Base camera
+//
+Camera::~Camera() {}
+
+static Matrix4 scale2DMatrix(float scale) {
+	Matrix4 m;
+	m(0, 0) = scale;
+	m(1, 1) = scale;
+	return m;
+}
+
+void Camera::setupMatricesAround(Vector3d center) {
+	Matrix4 matTemp;
+	matTemp.buildAroundZ(rotation());
+	_mat3Dto2D.setToIdentity();
+	_mat3Dto2D.translate(-center);
+	_mat3Dto2D = matTemp * _mat3Dto2D;
+	_mat3Dto2D = scale2DMatrix(scale()) * _mat3Dto2D;
+
+	_mat2Dto3D.setToIdentity();
+	_mat2Dto3D.translate(center);
+	matTemp.buildAroundZ(-rotation());
+	matTemp = matTemp * scale2DMatrix(1 / scale());
+	_mat2Dto3D = _mat2Dto3D * matTemp;
+}
+
+void minmax(Vector3d &min, Vector3d &max, Vector3d val) {
+	min.set(
+		MIN(min.x(), val.x()),
+		MIN(min.y(), val.y()),
+		MIN(min.z(), val.z()));
+	max.set(
+		MAX(max.x(), val.x()),
+		MAX(max.y(), val.y()),
+		MAX(max.z(), val.z()));
+}
+
+Vector3d Camera::setAppliedCenter(Vector3d center) {
+	setupMatricesAround(center);
+	if (g_engine->game().shouldClipCamera()) {
+		const float screenW = g_system->getWidth(), screenH = g_system->getHeight();
+		Vector3d min, max;
+		min = max = transform2Dto3D(Vector3d(0, 0, _roomScale));
+		minmax(min, max, transform2Dto3D(Vector3d(screenW, 0, _roomScale)));
+		minmax(min, max, transform2Dto3D(Vector3d(screenW, screenH, _roomScale)));
+		minmax(min, max, transform2Dto3D(Vector3d(0, screenH, _roomScale)));
+		center.x() += MAX(0.0f, _roomMin.getX() - min.x());
+		center.y() += MAX(0.0f, _roomMin.getY() - min.y());
+		center.x() -= MAX(0.0f, max.x() - _roomMax.getX());
+		center.y() -= MAX(0.0f, max.y() - _roomMax.getY());
+		setupMatricesAround(center);
+	}
+	return _appliedCenter = center;
+}
+
+Vector3d Camera::transform2Dto3D(Vector3d v2d) const {
+	// if this looks like normal 3D math to *someone* please contact.
+	Vector4d vh;
+	vh.w() = 1.0f;
+	vh.z() = v2d.z() - _appliedCenter.z();
+	vh.y() = (v2d.y() - g_system->getHeight() * 0.5f) * vh.z() * kInvBaseScale;
+	vh.x() = (v2d.x() - g_system->getWidth() * 0.5f) * vh.z() * kInvBaseScale;
+	vh = _mat2Dto3D * vh;
+	return Vector3d(vh.x(), vh.y(), 0.0f);
+}
+
+Vector3d Camera::transform3Dto2D(Vector3d v3d) const {
+	// I swear there is a better way than this. This is stupid. But it is original.
+	float depthScale = v3d.z() * kInvBaseScale;
+	Vector4d vh;
+	vh.x() = v3d.x() * depthScale + (1 - depthScale) * g_system->getWidth() * 0.5f;
+	vh.y() = v3d.y() * depthScale + (1 - depthScale) * g_system->getHeight() * 0.5f;
+	vh.z() = v3d.z();
+	vh.w() = 1.0f;
+	vh = _mat3Dto2D * vh;
+	return Vector3d(
+		g_system->getWidth() * 0.5f + vh.x() * kBaseScale / vh.z(),
+		g_system->getHeight() * 0.5f + vh.y() * kBaseScale / vh.z(),
+		scale() * kBaseScale / vh.z());
+}
+
+Point Camera::transform3Dto2D(Point p3d) const {
+	auto v2d = transform3Dto2D({ (float)p3d.x, (float)p3d.y, kBaseScale });
+	return { (int16)v2d.x(), (int16)v2d.y() };
+}
+
+//
+// CameraV3
+//
+
+Angle CameraV3::rotation() const {
+	return _cur._rotation;
+}
+
+float CameraV3::scale() const {
+	return _cur._scale;
+}
+
+void CameraV3::preUpdate() {
 	_shake = {};
 }
 
-void Camera::setRoomBounds(Graphic &background) {
+void CameraV3::setRoomBounds(Graphic &background) {
 	auto bgSize = background.animation().imageSize(0);
 	if (g_engine->isV1()) {
 		Point screenSize(g_system->getWidth(), g_system->getHeight());
@@ -62,7 +161,7 @@ void Camera::setRoomBounds(Graphic &background) {
 	}
 }
 
-void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
+void CameraV3::setFollow(WalkingCharacter *target, bool catchUp) {
 	_cur._isFollowingTarget = target != nullptr;
 	_followTarget = target;
 	_lastUpdateTime = g_engine->getMillis();
@@ -71,153 +170,71 @@ void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
 		_isChanging = false;
 }
 
-void Camera::onChangedRoom(bool resetCamera) {
+void CameraV3::onChangedRoom(bool resetCamera) {
 	if (resetCamera)
 		resetRotationAndScale();
 	if (_followTarget != nullptr)
 		setFollow(_followTarget, true);
 }
 
-void Camera::onTriggeredDoor(WalkingCharacter *target) {
+void CameraV3::onTriggeredDoor(WalkingCharacter *target) {
 	setFollow(target, true);
 }
 
-void Camera::onTriggeredDoor(Point fixedPosition) {
+void CameraV3::onTriggeredDoor(Point fixedPosition) {
 	setPosition(as2D(fixedPosition));
 }
 
-void Camera::onScriptChangedCharacter(MainCharacterKind kind) {
+void CameraV3::onScriptChangedCharacter(MainCharacterKind kind) {
 	resetRotationAndScale();
 	if (kind != MainCharacterKind::None)
 		setFollow(g_engine->player().activeCharacter());
 	backup(0);
 }
 
-void Camera::onUserChangedCharacter() {
+void CameraV3::onUserChangedCharacter() {
 	setFollow(g_engine->player().activeCharacter());
 	restore(0);
 }
 
-void Camera::onOpenMenu() {
+void CameraV3::onOpenMenu() {
 	backup(1);
 	setPosition(Math::Vector3d(
 		g_system->getWidth() / 2.0f, g_system->getHeight() / 2.0f, 0.0f));
 }
 
-void Camera::onCloseMenu() {
+void CameraV3::onCloseMenu() {
 	restore(1);
 }
 
-void Camera::resetRotationAndScale() {
+void CameraV3::resetRotationAndScale() {
 	_cur._scale = 1;
 	_cur._rotation = 0;
 	_cur._usedCenter.z() = 0;
 }
 
-void Camera::setPosition(Vector2d v) {
+void CameraV3::setPosition(Vector2d v) {
 	setPosition({ v.getX(), v.getY(), _cur._usedCenter.z() });
 }
 
-void Camera::setPosition(Vector3d v) {
+void CameraV3::setPosition(Vector3d v) {
 	_cur._usedCenter = v;
 	setFollow(nullptr);
 }
 
-void Camera::backup(uint slot) {
+void CameraV3::backup(uint slot) {
 	assert(slot < kStateBackupCount);
 	_backups[slot] = _cur;
 }
 
-void Camera::restore(uint slot) {
+void CameraV3::restore(uint slot) {
 	assert(slot < kStateBackupCount);
 	auto backupState = _backups[slot];
 	_backups[slot] = _cur;
 	_cur = backupState;
 }
 
-static Matrix4 scale2DMatrix(float scale) {
-	Matrix4 m;
-	m(0, 0) = scale;
-	m(1, 1) = scale;
-	return m;
-}
-
-void Camera::setupMatricesAround(Vector3d center) {
-	Matrix4 matTemp;
-	matTemp.buildAroundZ(_cur._rotation);
-	_mat3Dto2D.setToIdentity();
-	_mat3Dto2D.translate(-center);
-	_mat3Dto2D = matTemp * _mat3Dto2D;
-	_mat3Dto2D = scale2DMatrix(_cur._scale) * _mat3Dto2D;
-
-	_mat2Dto3D.setToIdentity();
-	_mat2Dto3D.translate(center);
-	matTemp.buildAroundZ(-_cur._rotation);
-	matTemp = matTemp * scale2DMatrix(1 / _cur._scale);
-	_mat2Dto3D = _mat2Dto3D * matTemp;
-}
-
-void minmax(Vector3d &min, Vector3d &max, Vector3d val) {
-	min.set(
-		MIN(min.x(), val.x()),
-		MIN(min.y(), val.y()),
-		MIN(min.z(), val.z()));
-	max.set(
-		MAX(max.x(), val.x()),
-		MAX(max.y(), val.y()),
-		MAX(max.z(), val.z()));
-}
-
-Vector3d Camera::setAppliedCenter(Vector3d center) {
-	setupMatricesAround(center);
-	if (g_engine->game().shouldClipCamera()) {
-		const float screenW = g_system->getWidth(), screenH = g_system->getHeight();
-		Vector3d min, max;
-		min = max = transform2Dto3D(Vector3d(0, 0, _roomScale));
-		minmax(min, max, transform2Dto3D(Vector3d(screenW, 0, _roomScale)));
-		minmax(min, max, transform2Dto3D(Vector3d(screenW, screenH, _roomScale)));
-		minmax(min, max, transform2Dto3D(Vector3d(0, screenH, _roomScale)));
-		center.x() += MAX(0.0f, _roomMin.getX() - min.x());
-		center.y() += MAX(0.0f, _roomMin.getY() - min.y());
-		center.x() -= MAX(0.0f, max.x() - _roomMax.getX());
-		center.y() -= MAX(0.0f, max.y() - _roomMax.getY());
-		setupMatricesAround(center);
-	}
-	return _appliedCenter = center;
-}
-
-Vector3d Camera::transform2Dto3D(Vector3d v2d) const {
-	// if this looks like normal 3D math to *someone* please contact.
-	Vector4d vh;
-	vh.w() = 1.0f;
-	vh.z() = v2d.z() - _cur._usedCenter.z();
-	vh.y() = (v2d.y() - g_system->getHeight() * 0.5f) * vh.z() * kInvBaseScale;
-	vh.x() = (v2d.x() - g_system->getWidth() * 0.5f) * vh.z() * kInvBaseScale;
-	vh = _mat2Dto3D * vh;
-	return Vector3d(vh.x(), vh.y(), 0.0f);
-}
-
-Vector3d Camera::transform3Dto2D(Vector3d v3d) const {
-	// I swear there is a better way than this. This is stupid. But it is original.
-	float depthScale = v3d.z() * kInvBaseScale;
-	Vector4d vh;
-	vh.x() = v3d.x() * depthScale + (1 - depthScale) * g_system->getWidth() * 0.5f;
-	vh.y() = v3d.y() * depthScale + (1 - depthScale) * g_system->getHeight() * 0.5f;
-	vh.z() = v3d.z();
-	vh.w() = 1.0f;
-	vh = _mat3Dto2D * vh;
-	return Vector3d(
-		g_system->getWidth() * 0.5f + vh.x() * kBaseScale / vh.z(),
-		g_system->getHeight() * 0.5f + vh.y() * kBaseScale / vh.z(),
-		_cur._scale * kBaseScale / vh.z());
-}
-
-Point Camera::transform3Dto2D(Point p3d) const {
-	auto v2d = transform3Dto2D({ (float)p3d.x, (float)p3d.y, kBaseScale });
-	return { (int16)v2d.x(), (int16)v2d.y() };
-}
-
-void Camera::update() {
+void CameraV3::update() {
 	// original would be some smoothing of delta times, let's not.
 	uint32 now = g_engine->getMillis();
 	float deltaTime = (now - _lastUpdateTime) / 1000.0f;
@@ -233,7 +250,7 @@ void Camera::update() {
 	setAppliedCenter(_cur._usedCenter + Vector3d(_shake.getX(), _shake.getY(), 0.0f));
 }
 
-void Camera::updateFollowing(float deltaTime) {
+void CameraV3::updateFollowing(float deltaTime) {
 	if (!_cur._isFollowingTarget || _followTarget == nullptr)
 		return;
 	const float resolutionFactor = g_system->getWidth() * 0.00125f;
@@ -299,7 +316,7 @@ static void syncVector(Serializer &s, Vector3d &v) {
 	s.syncAsFloatLE(v.z());
 }
 
-void Camera::State::syncGame(Serializer &s) {
+void CameraV3::State::syncGame(Serializer &s) {
 	syncVector(s, _usedCenter);
 	s.syncAsFloatLE(_scale);
 	s.syncAsFloatLE(_speed);
@@ -311,7 +328,7 @@ void Camera::State::syncGame(Serializer &s) {
 	s.syncAsByte(_isFollowingTarget);
 }
 
-void Camera::syncGame(Serializer &s) {
+void CameraV3::syncGame(Serializer &s) {
 	syncMatrix(s, _mat3Dto2D);
 	syncMatrix(s, _mat2Dto3D);
 	syncVector(s, _appliedCenter);
@@ -344,7 +361,7 @@ void Camera::syncGame(Serializer &s) {
 struct CamLerpTask : public Task {
 	CamLerpTask(Process &process, uint32 duration = 0, EasingType easingType = EasingType::Linear)
 		: Task(process)
-		, _camera(g_engine->camera())
+		, _camera(g_engine->cameraV3())
 		, _duration(duration)
 		, _easingType(easingType) {}
 
@@ -377,7 +394,7 @@ struct CamLerpTask : public Task {
 protected:
 	virtual void update(float t) = 0;
 
-	Camera &_camera;
+	CameraV3 &_camera;
 	uint32 _startTime = 0, _duration;
 	EasingType _easingType;
 };
@@ -550,11 +567,11 @@ DECLARE_TASK(CamShakeTask)
 struct CamWaitToStopTask final : public Task {
 	CamWaitToStopTask(Process &process)
 		: Task(process)
-		, _camera(g_engine->camera()) {}
+		, _camera(g_engine->cameraV3()) {}
 
 	CamWaitToStopTask(Process &process, Serializer &s)
 		: Task(process)
-		, _camera(g_engine->camera()) {
+		, _camera(g_engine->cameraV3()) {
 		syncGame(s);
 	}
 
@@ -571,7 +588,7 @@ struct CamWaitToStopTask final : public Task {
 	const char *taskName() const override;
 
 private:
-	Camera &_camera;
+	CameraV3 &_camera;
 };
 DECLARE_TASK(CamWaitToStopTask)
 
@@ -584,14 +601,14 @@ struct CamSetInactiveAttributeTask final : public Task {
 
 	CamSetInactiveAttributeTask(Process &process, Attribute attribute, float value, int32 delay)
 		: Task(process)
-		, _camera(g_engine->camera())
+		, _camera(g_engine->cameraV3())
 		, _attribute(attribute)
 		, _value(value)
 		, _delay(delay) {}
 
 	CamSetInactiveAttributeTask(Process &process, Serializer &s)
 		: Task(process)
-		, _camera(g_engine->camera()) {
+		, _camera(g_engine->cameraV3()) {
 		syncGame(s);
 	}
 
@@ -649,14 +666,14 @@ struct CamSetInactiveAttributeTask final : public Task {
 	const char *taskName() const override;
 
 private:
-	Camera &_camera;
+	CameraV3 &_camera;
 	Attribute _attribute = {};
 	float _value = 0;
 	int32 _delay = 0;
 };
 DECLARE_TASK(CamSetInactiveAttributeTask)
 
-Task *Camera::lerpPos(Process &process,
+Task *CameraV3::lerpPos(Process &process,
 					  Vector2d targetPos,
 					  int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
@@ -666,7 +683,7 @@ Task *Camera::lerpPos(Process &process,
 	return new CamLerpPosTask(process, targetPos3d, duration, easingType);
 }
 
-Task *Camera::lerpPos(Process &process,
+Task *CameraV3::lerpPos(Process &process,
 					  Vector3d targetPos,
 					  int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
@@ -676,7 +693,7 @@ Task *Camera::lerpPos(Process &process,
 	return new CamLerpPosTask(process, targetPos, duration, easingType);
 }
 
-Task *Camera::lerpPosZ(Process &process,
+Task *CameraV3::lerpPosZ(Process &process,
 					   float targetPosZ,
 					   int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
@@ -686,7 +703,7 @@ Task *Camera::lerpPosZ(Process &process,
 	return new CamLerpPosTask(process, targetPos, duration, easingType);
 }
 
-Task *Camera::lerpScale(Process &process,
+Task *CameraV3::lerpScale(Process &process,
 						float targetScale,
 						int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
@@ -695,7 +712,7 @@ Task *Camera::lerpScale(Process &process,
 	return new CamLerpScaleTask(process, targetScale, duration, easingType);
 }
 
-Task *Camera::lerpRotation(Process &process,
+Task *CameraV3::lerpRotation(Process &process,
 						   float targetRotation,
 						   int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
@@ -704,7 +721,7 @@ Task *Camera::lerpRotation(Process &process,
 	return new CamLerpRotationTask(process, targetRotation, duration, easingType);
 }
 
-Task *Camera::lerpPosScale(Process &process,
+Task *CameraV3::lerpPosScale(Process &process,
 						   Vector3d targetPos, float targetScale,
 						   int32 duration,
 						   EasingType moveEasingType, EasingType scaleEasingType) {
@@ -714,11 +731,11 @@ Task *Camera::lerpPosScale(Process &process,
 	return new CamLerpPosScaleTask(process, targetPos, targetScale, duration, moveEasingType, scaleEasingType);
 }
 
-Task *Camera::waitToStop(Process &process) {
+Task *CameraV3::waitToStop(Process &process) {
 	return new CamWaitToStopTask(process);
 }
 
-Task *Camera::shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration) {
+Task *CameraV3::shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration) {
 	if (!process.isActiveForPlayer()) {
 		return new DelayTask(process, (uint32)duration);
 	}
@@ -731,12 +748,12 @@ Task *Camera::shake(Process &process, Math::Vector2d amplitude, Math::Vector2d f
 struct CamDisguiseTask final : public Task {
 	CamDisguiseTask(Process &process, int32 durationMs)
 		: Task(process)
-		, _camera(g_engine->camera())
+		, _camera(g_engine->cameraV3())
 		, _durationMs(durationMs) {}
 
 	CamDisguiseTask(Process &process, Serializer &s)
 		: Task(process)
-		, _camera(g_engine->camera()) {
+		, _camera(g_engine->cameraV3()) {
 		CamDisguiseTask::syncGame(s);
 	}
 
@@ -776,14 +793,14 @@ struct CamDisguiseTask final : public Task {
 	const char *taskName() const override;
 
 private:
-	Camera &_camera;
+	CameraV3 &_camera;
 	int32 _durationMs = 0;
 	uint32 _startTime = 0;
 	Vector2d _startPosition;
 };
 DECLARE_TASK(CamDisguiseTask)
 
-Task *Camera::disguise(Process &process, int32 duration) {
+Task *CameraV3::disguise(Process &process, int32 duration) {
 	return new CamDisguiseTask(process, duration);
 }
 
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index e7424587f51..296d98d4db0 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -37,25 +37,59 @@ static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
 
 class Camera {
 public:
-	inline Math::Angle rotation() const { return _cur._rotation; }
-
-	void preUpdate();
-	void update();
-	void setRoomBounds(Graphic &background);
-	void setFollow(WalkingCharacter *target, bool catchUp = false);
-	void onChangedRoom(bool resetCamera);
-	void onTriggeredDoor(WalkingCharacter *target);
-	void onTriggeredDoor(Common::Point fixedPosition);
-	void onScriptChangedCharacter(MainCharacterKind kind);
-	void onUserChangedCharacter();
-	void onOpenMenu();
-	void onCloseMenu();
-	void syncGame(Common::Serializer &s);
+	virtual ~Camera();
+	virtual Math::Angle rotation() const = 0;
+	virtual float scale() const = 0;
+
+	virtual void preUpdate() = 0;
+	virtual void update() = 0;
+	virtual void setRoomBounds(Graphic &background) = 0;
+	virtual void setFollow(WalkingCharacter *target, bool catchUp = false) = 0;
+	virtual void onChangedRoom(bool resetCamera) = 0;
+	virtual void onTriggeredDoor(WalkingCharacter *target) = 0;
+	virtual void onTriggeredDoor(Common::Point fixedPosition) = 0;
+	virtual void onScriptChangedCharacter(MainCharacterKind kind) = 0;
+	virtual void onUserChangedCharacter() = 0;
+	virtual void onOpenMenu() = 0;
+	virtual void onCloseMenu() = 0;
+	virtual void syncGame(Common::Serializer &s) = 0;
 
 	Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
 	Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
 	Common::Point transform3Dto2D(Common::Point p) const;
 
+protected:
+	Math::Vector3d setAppliedCenter(Math::Vector3d center);
+	void setupMatricesAround(Math::Vector3d center);
+
+	float _roomScale = 1.0f;
+	Math::Vector2d
+		_roomMin = Math::Vector2d(-10000, -10000),
+		_roomMax = Math::Vector2d(10000, 10000);
+	Math::Vector3d _appliedCenter;
+	Math::Matrix4
+		_mat3Dto2D,
+		_mat2Dto3D;
+};
+
+class CameraV3 : public Camera {
+public:
+	Math::Angle rotation() const override;
+	float scale() const override;
+
+	void preUpdate() override;
+	void update() override;
+	void setRoomBounds(Graphic &background) override;
+	void setFollow(WalkingCharacter *target, bool catchUp = false) override;
+	void onChangedRoom(bool resetCamera) override;
+	void onTriggeredDoor(WalkingCharacter *target) override;
+	void onTriggeredDoor(Common::Point fixedPosition) override;
+	void onScriptChangedCharacter(MainCharacterKind kind) override;
+	void onUserChangedCharacter() override;
+	void onOpenMenu() override;
+	void onCloseMenu() override;
+	void syncGame(Common::Serializer &s) override;
+
 	Task *lerpPos(Process &process,
 		Math::Vector2d targetPos,
 		int32 duration, EasingType easingType);
@@ -94,8 +128,6 @@ private:
 	friend struct CamWaitToStopTask;
 	friend struct CamSetInactiveAttributeTask;
 	friend struct CamDisguiseTask;
-	Math::Vector3d setAppliedCenter(Math::Vector3d center);
-	void setupMatricesAround(Math::Vector3d center);
 	void updateFollowing(float deltaTime);
 
 	struct State {
@@ -117,15 +149,7 @@ private:
 	uint32 _lastUpdateTime = 0;
 	bool _isChanging = false,
 		_catchUp = false;
-	float _roomScale = 1.0f;
-	Math::Vector2d
-		_roomMin = Math::Vector2d(-10000, -10000),
-		_roomMax = Math::Vector2d(10000, 10000),
-		_shake;
-	Math::Vector3d _appliedCenter;
-	Math::Matrix4
-		_mat3Dto2D,
-		_mat2Dto3D;
+	Math::Vector2d _shake;
 };
 
 }
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 578ea0f23cb..6da4d7e23d6 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -932,7 +932,7 @@ private:
 
 		// Camera tasks
 		case ScriptKernelTask::WaitCamStopping:
-			return TaskReturn::waitFor(g_engine->camera().waitToStop(process()));
+			return TaskReturn::waitFor(g_engine->cameraV3().waitToStop(process()));
 		case ScriptKernelTask::CamFollow: {
 			WalkingCharacter *target = nullptr;
 			auto kind = getMainCharacterKindArg(0);
@@ -942,28 +942,28 @@ private:
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::CamShake:
-			return TaskReturn::waitFor(g_engine->camera().shake(process(),
+			return TaskReturn::waitFor(g_engine->cameraV3().shake(process(),
 				Vector2d(getNumberArg(1), getNumberArg(2)),
 				Vector2d(getNumberArg(3), getNumberArg(4)),
 				getNumberArg(0)));
 		case ScriptKernelTask::LerpCamXY:
-			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
+			return TaskReturn::waitFor(g_engine->cameraV3().lerpPos(process(),
 				Vector2d(getNumberArg(0), getNumberArg(1)),
 				getNumberArg(2), (EasingType)getNumberArg(3)));
 		case ScriptKernelTask::LerpCamXYZ:
-			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
+			return TaskReturn::waitFor(g_engine->cameraV3().lerpPos(process(),
 				Vector3d(getNumberArg(0), getNumberArg(1), getNumberArg(2)),
 				getNumberArg(3), (EasingType)getNumberArg(4)));
 		case ScriptKernelTask::LerpCamZ:
-			return TaskReturn::waitFor(g_engine->camera().lerpPosZ(process(),
+			return TaskReturn::waitFor(g_engine->cameraV3().lerpPosZ(process(),
 				getNumberArg(0),
 				getNumberArg(1), (EasingType)getNumberArg(2)));
 		case ScriptKernelTask::LerpCamScale:
-			return TaskReturn::waitFor(g_engine->camera().lerpScale(process(),
+			return TaskReturn::waitFor(g_engine->cameraV3().lerpScale(process(),
 				getNumberArg(0) * 0.01f,
 				getNumberArg(1), (EasingType)getNumberArg(2)));
 		case ScriptKernelTask::LerpCamRotation:
-			return TaskReturn::waitFor(g_engine->camera().lerpRotation(process(),
+			return TaskReturn::waitFor(g_engine->cameraV3().lerpRotation(process(),
 				getNumberArg(0),
 				getNumberArg(1), (EasingType)getNumberArg(2)));
 		case ScriptKernelTask::LerpCamToObjectKeepingZ: {
@@ -974,7 +974,7 @@ private:
 				pointObject = g_engine->game().unknownCamLerpTarget("LerpCamToObjectKeepingZ", getStringArg(0));
 			if (pointObject == nullptr)
 				return TaskReturn::finish(1);
-			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
+			return TaskReturn::waitFor(g_engine->cameraV3().lerpPos(process(),
 				as2D(pointObject->position()),
 				getNumberArg(1), EasingType::Linear));
 		}
@@ -986,7 +986,7 @@ private:
 				pointObject = g_engine->game().unknownCamLerpTarget("LerpCamToObjectResettingZ", getStringArg(0));
 			if (pointObject == nullptr)
 				return TaskReturn::finish(1);
-			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
+			return TaskReturn::waitFor(g_engine->cameraV3().lerpPos(process(),
 				as3D(pointObject->position()),
 				getNumberArg(1), (EasingType)getNumberArg(2)));
 		}
@@ -994,13 +994,13 @@ private:
 			float targetScale = getNumberArg(1) * 0.01f;
 			if (!process().isActiveForPlayer())
 				// the scale will wait then snap the scale
-				return TaskReturn::waitFor(g_engine->camera().lerpScale(process(), targetScale, getNumberArg(2), EasingType::Linear));
+				return TaskReturn::waitFor(g_engine->cameraV3().lerpScale(process(), targetScale, getNumberArg(2), EasingType::Linear));
 			auto pointObject = getObjectArg<PointObject>(0);
 			if (pointObject == nullptr)
 				pointObject = g_engine->game().unknownCamLerpTarget("LerpCamToObjectWithScale", getStringArg(0));
 			if (pointObject == nullptr)
 				return TaskReturn::finish(1);
-			return TaskReturn::waitFor(g_engine->camera().lerpPosScale(process(),
+			return TaskReturn::waitFor(g_engine->cameraV3().lerpPosScale(process(),
 				as3D(pointObject->position()), targetScale,
 				getNumberArg(2), (EasingType)getNumberArg(3), (EasingType)getNumberArg(4)));
 		}
@@ -1010,7 +1010,7 @@ private:
 			const auto duration = getNumberArg(0);
 			return TaskReturn::waitFor(duration == 0
 				? g_engine->input().waitForInput(process())
-				: g_engine->camera().disguise(process(), duration));
+				: g_engine->cameraV3().disguise(process(), duration));
 		}
 
 		// Fades


Commit: 8ade46170b80548687a7f834c1514fdd219f4ee5
    https://github.com/scummvm/scummvm/commit/8ade46170b80548687a7f834c1514fdd219f4ee5
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-08T19:01:39+01:00

Commit Message:
ALCACHOFA: TERROR: Fix outro

Changed paths:
    engines/alcachofa/game-movie-adventure-original.cpp


diff --git a/engines/alcachofa/game-movie-adventure-original.cpp b/engines/alcachofa/game-movie-adventure-original.cpp
index d6904937a45..af0a23c11e3 100644
--- a/engines/alcachofa/game-movie-adventure-original.cpp
+++ b/engines/alcachofa/game-movie-adventure-original.cpp
@@ -88,7 +88,7 @@ static constexpr const ScriptKernelTask kScriptKernelTaskMap[] = {
 	ScriptKernelTask::Animate,
 	ScriptKernelTask::HadNoMousePressFor,
 	ScriptKernelTask::ChangeCharacter,
-	ScriptKernelTask::LerpCamToObjectKeepingZ,
+	ScriptKernelTask::LerpOrSetCam,
 	ScriptKernelTask::Drop,
 	ScriptKernelTask::ChangeDoor,
 	ScriptKernelTask::Disguise,
@@ -327,12 +327,17 @@ public:
 			return nullptr;
 		}
 
-		// an original bug, Pos_Final_Morta/File is defined in room ENTRADA_PUEBLO
+		// an original bug, Pos_Final_Morta/File is defined in room ENTRADA_PUEBLO or PANTANO_EXT
 		// but the current room is ENTRADA_PUEBLO_INTRO
-		if (!scumm_stricmp(name, "Pos_Final_Morta"))
-			return getPointFromRoom("ENTRADA_PUEBLO", "Pos_Final_Morta", action);
-		if (!scumm_stricmp(name, "Pos_Final_File"))
-			return getPointFromRoom("ENTRADA_PUEBLO", "Pos_Final_File", action);
+		if (!scumm_stricmp(name, "Pos_Final_Morta") || !scumm_stricmp(name, "Pos_Final_File")) {
+			// for terror there is no ENTRADA_PUEBLO either, nor Pos_Final_*
+			if (g_engine->world().getRoomByName("ENTRADA_PUEBLO") == nullptr) {
+				return !scumm_stricmp(name, "Pos_Final_Morta")
+					? getPointFromRoom("PANTANO_EXT", "PANTANO_MORTA", action)
+					: getPointFromRoom("PANTANO_EXT", "PANTANO_FILE", action);
+			} else
+				return getPointFromRoom("ENTRADA_PUEBLO", name, action);
+		}
 
 		return Game::unknownGoPutTarget(process, action, name);
 	}
@@ -346,7 +351,11 @@ public:
 	}
 
 	void missingSound(const Common::String &fileName) override {
-		if (fileName == "CHAS" || fileName == "0563" || fileName == "M2137")
+		if (fileName == "CHAS" ||
+			fileName == "0563" ||
+			fileName == "M2137" ||
+			fileName == "1413" || // are stored in OESTE.EMC but played during terror outro
+			fileName == "M1414")
 			return;
 		return Game::missingSound(fileName);
 	}


Commit: 461eeec147ba2265a1eb8874817d88a721a32759
    https://github.com/scummvm/scummvm/commit/461eeec147ba2265a1eb8874817d88a721a32759
Author: Helco (hermann.noll at hotmail.com)
Date: 2026-02-08T19:01:39+01:00

Commit Message:
ALCACHOFA: Add CameraV1

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/script.h
    engines/alcachofa/tasks.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index a921d76c421..c36a0da111c 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -84,7 +84,7 @@ Common::Error AlcachofaEngine::run() {
 	_world.load();
 	_renderer.reset(IRenderer::createOpenGLRenderer(game().getResolution()));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
-	_camera.reset(new CameraV3());
+	_camera.reset(isV1() ? static_cast<Camera *>(new CameraV1()) : new CameraV3());
 	_script.reset(new Script());
 	_player.reset(new Player());
 	_globalUI.reset(isV1() ? static_cast<GlobalUI *>(new GlobalUIV1()) : new GlobalUIV3());
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index a75acdf8673..0cc423bc455 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -133,11 +133,13 @@ public:
 	inline Config &config() { return _config; }
 	inline bool isDebugModeActive() const { return _debugHandler != nullptr; }
 
-	inline CameraV3 &cameraV3() {
-		auto result = dynamic_cast<CameraV3 *>(_camera.get());
+	template<class T> inline T &cameraAs() {
+		auto result = dynamic_cast<T *>(_camera.get());
 		scumm_assert(result != nullptr);
 		return *result;
 	}
+	inline CameraV1 &cameraV1() { return cameraAs<CameraV1>(); }
+	inline CameraV3 &cameraV3() { return cameraAs<CameraV3>(); }
 
 	uint32 getMillis() const;
 	void setMillis(uint32 newMillis);
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 46db22db5ba..3bf2a86d285 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -118,6 +118,230 @@ Point Camera::transform3Dto2D(Point p3d) const {
 	return { (int16)v2d.x(), (int16)v2d.y() };
 }
 
+static void syncMatrix(Serializer &s, Matrix4 &m) {
+	float *data = m.getData();
+	for (int i = 0; i < 16; i++)
+		s.syncAsFloatLE(data[i]);
+}
+
+static void syncVector(Serializer &s, Vector3d &v) {
+	s.syncAsFloatLE(v.x());
+	s.syncAsFloatLE(v.y());
+	s.syncAsFloatLE(v.z());
+}
+
+static void syncFollowTarget(Serializer &s, WalkingCharacter *&followTarget) {
+	// originally the follow object is also searched for before changing the room
+	// so that would practically mean only the main characters could be reasonably found
+	// instead we fall back to global search
+	String name;
+	if (followTarget != nullptr)
+		name = followTarget->name();
+	s.syncString(name);
+	if (s.isLoading()) {
+		if (name.empty())
+			followTarget = nullptr;
+		else {
+			followTarget = dynamic_cast<WalkingCharacter *>(g_engine->world().getObjectByName(name.c_str()));
+			if (followTarget == nullptr)
+				followTarget = dynamic_cast<WalkingCharacter *>(g_engine->world().getObjectByNameFromAnyRoom(name.c_str()));
+			if (followTarget == nullptr)
+				warning("Camera follow target from savestate was not found: %s", name.c_str());
+		}
+	}
+}
+
+//
+// CameraV1
+//
+
+Angle CameraV1::rotation() const {
+	return {};
+}
+
+float CameraV1::scale() const {
+	return 1.0f;
+}
+
+void CameraV1::preUpdate() {
+}
+
+void CameraV1::update() {
+	auto deltaTime = (g_engine->getMillis() - _lastUpdateTime) / 1000.0f;
+	auto newCenter = _appliedCenter;
+
+	if (_followTarget != nullptr) {
+		// this threshold is responsible for the jitter while following
+		const auto threshold = _isLerping ? 100 : 200;
+		_target = as3D(_followTarget->position());
+		auto deltaPos = _target - newCenter;
+		_lastUpdateTime = g_engine->getMillis();
+
+		_isLerping = false;
+		if (fabsf(deltaPos.x()) > threshold) {
+			newCenter.x() += copysignf(350.0f, deltaPos.x()) * deltaTime;
+			_isLerping = true;
+		}
+		if (fabsf(deltaPos.y()) > threshold) {
+			newCenter.y() += copysignf(350.0f, deltaPos.y()) * deltaTime;
+			_isLerping = true;
+		}
+	} else if (_isLerping) {
+		auto distance = newCenter.getDistanceTo(_target);
+		auto move = deltaTime * _lerpSpeed;
+		_lastUpdateTime = g_engine->getMillis();
+
+		if (move < distance)
+			newCenter += (_target - newCenter) / distance * move;
+		else {
+			newCenter = _target;
+			_isLerping = false;
+		}
+	}
+
+	setAppliedCenter(newCenter);
+}
+
+void CameraV1::setRoomBounds(Graphic &background) {
+	auto bgSize = background.animation().imageSize(0);
+	Point screenSize(g_system->getWidth(), g_system->getHeight());
+	_roomMin = as2D(background.topLeft() + screenSize / 2);
+	_roomMax = _roomMin + as2D(bgSize - screenSize);
+	_roomScale = 0;
+}
+
+void CameraV1::setFollow(WalkingCharacter *target) {
+	_lastUpdateTime = g_engine->getMillis();
+	_followTarget = target;
+	_isLerping = false;
+	if (target != nullptr)
+		setAppliedCenter(as3D(target->position()));
+}
+
+void CameraV1::onChangedRoom(bool resetCamera) {
+	// nothing to do in V1
+}
+
+void CameraV1::onTriggeredDoor(WalkingCharacter *target) {
+	setFollow(target);
+}
+
+void CameraV1::onTriggeredDoor(Common::Point fixedPosition) {
+	// should probably never be called
+	debug(1, "Set camera to fixed position in V1: %d, %d", fixedPosition.x, fixedPosition.y);
+}
+
+void CameraV1::onScriptChangedCharacter(MainCharacterKind kind) {
+	if (kind != MainCharacterKind::None)
+		setFollow(g_engine->player().activeCharacter());
+}
+
+void CameraV1::onUserChangedCharacter() {
+	setFollow(g_engine->player().activeCharacter());
+}
+
+void CameraV1::onOpenMenu() {
+	// we rely on room bounds clipping to set the camera position
+	// interaction locks prevent opening menus during lerps, follows are fine due to clipping 
+}
+
+void CameraV1::onCloseMenu() {
+}
+
+void CameraV1::syncGame(Serializer &s) {
+	syncVector(s, _appliedCenter);
+	syncMatrix(s, _mat3Dto2D);
+	syncMatrix(s, _mat2Dto3D);
+	syncFollowTarget(s, _followTarget);
+	syncVector(s, _target);
+	s.syncAsByte(_isLerping);
+	s.syncAsFloatLE(_lerpSpeed);
+	s.syncAsUint32LE(_lastUpdateTime);
+} 
+
+void CameraV1::lerpOrSet(Point target, int32 mode) {
+	_target = as3D(target);
+	_lastUpdateTime = g_engine->getMillis();
+	_followTarget = nullptr;
+	_isLerping = true;
+
+	if (mode == 1) {
+		// snap to target
+		_isLerping = false;
+		_appliedCenter = _target;
+	} else if (mode <= 0) {
+		// fixed speed, overshoot target
+		_target.x() += copysignf(100, _appliedCenter.x() - _target.x());
+		_target.y() += copysignf(100, _appliedCenter.y() - _target.y());
+		_lerpSpeed = 350.0f;
+	} else {
+		// dynamic speed
+		_lerpSpeed = MAX(1.0f, _target.getDistanceTo(_appliedCenter) / mode);
+	}
+}
+
+// The original name for this task is "disfraza" which I can only translate as "disguise"
+// It is a slightly bouncing vertical camera movement with fixed distance
+
+struct CamV1DisguiseTask final : public Task {
+	CamV1DisguiseTask(Process &process, int32 durationMs)
+		: Task(process)
+		, _camera(g_engine->cameraV1())
+		, _durationMs(durationMs) {}
+
+	CamV1DisguiseTask(Process &process, Serializer &s)
+		: Task(process)
+		, _camera(g_engine->cameraV1()) {
+		CamV1DisguiseTask::syncGame(s);
+	}
+
+	TaskReturn run() override {
+		if (_startTime == 0) {
+			_startPosition = _camera._appliedCenter;
+			_startTime = g_engine->getMillis();
+		}
+		if (_durationMs <= 0 || g_engine->getMillis() - _startTime >= (uint32)_durationMs)
+			return TaskReturn::finish(0);
+
+		Vector3d newPosition = _startPosition;
+		uint32 t = (g_engine->getMillis() - _startTime) * 5;
+		if (t <= 50)
+			newPosition.y() += t;
+		else if (t <= 150)
+			newPosition.y() += 100 - t;
+		else if (t >= 200)
+			newPosition.y() += t - 200;
+		_camera._appliedCenter = newPosition;
+		_camera.setFollow(nullptr);
+
+		return TaskReturn::yield();
+	}
+
+	void debugPrint() override {
+		g_engine->getDebugger()->debugPrintf("\"Disguise\" camera for %dms", _durationMs);
+	}
+
+	void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		s.syncAsSint32LE(_durationMs);
+		s.syncAsUint32LE(_startTime);
+		syncVector(s, _startPosition);
+	}
+
+	const char *taskName() const override;
+
+private:
+	CameraV1 &_camera;
+	int32 _durationMs = 0;
+	uint32 _startTime = 0;
+	Vector3d _startPosition;
+};
+DECLARE_TASK(CamV1DisguiseTask)
+
+Task *CameraV1::disguise(Process &process, int32 duration) {
+	return new CamV1DisguiseTask(process, duration);
+}
+
 //
 // CameraV3
 //
@@ -136,29 +360,26 @@ void CameraV3::preUpdate() {
 
 void CameraV3::setRoomBounds(Graphic &background) {
 	auto bgSize = background.animation().imageSize(0);
-	if (g_engine->isV1()) {
-		Point screenSize(g_system->getWidth(), g_system->getHeight());
-		_roomMin = as2D(background.topLeft() + screenSize / 2);
-		_roomMax = _roomMin + as2D(bgSize - screenSize);
-		_roomScale = 0;
-	} else {
-		/* The fallback fixes a bug where if the background image is invalid the original engine
-		 * would not update the background size. This would be around 1024,768 due to
-		 * previous rooms in the bug instances I found.
-		 */
-		if (bgSize == Point(0, 0))
-			bgSize = Point(1024, 768);
+	/* The fallback fixes a bug where if the background image is invalid the original engine
+		* would not update the background size. This would be around 1024,768 due to
+		* previous rooms in the bug instances I found.
+		*/
+	if (bgSize == Point(0, 0))
+		bgSize = Point(1024, 768);
 
-		const auto bgScale = background.scale();
-		float scaleFactor = 1 - bgScale * kInvBaseScale;
-		_roomMin = Vector2d(
-			g_system->getWidth() / 2 * scaleFactor,
-			g_system->getHeight() / 2 * scaleFactor);
-		_roomMax = _roomMin + Vector2d(
-			bgSize.x * bgScale * kInvBaseScale,
-			bgSize.y * bgScale * kInvBaseScale);
-		_roomScale = bgScale;
-	}
+	const auto bgScale = background.scale();
+	float scaleFactor = 1 - bgScale * kInvBaseScale;
+	_roomMin = Vector2d(
+		g_system->getWidth() / 2 * scaleFactor,
+		g_system->getHeight() / 2 * scaleFactor);
+	_roomMax = _roomMin + Vector2d(
+		bgSize.x * bgScale * kInvBaseScale,
+		bgSize.y * bgScale * kInvBaseScale);
+	_roomScale = bgScale;
+}
+
+void CameraV3::setFollow(WalkingCharacter *target) {
+	setFollow(target, false);
 }
 
 void CameraV3::setFollow(WalkingCharacter *target, bool catchUp) {
@@ -304,18 +525,6 @@ void CameraV3::updateFollowing(float deltaTime) {
 	}
 }
 
-static void syncMatrix(Serializer &s, Matrix4 &m) {
-	float *data = m.getData();
-	for (int i = 0; i < 16; i++)
-		s.syncAsFloatLE(data[i]);
-}
-
-static void syncVector(Serializer &s, Vector3d &v) {
-	s.syncAsFloatLE(v.x());
-	s.syncAsFloatLE(v.y());
-	s.syncAsFloatLE(v.z());
-}
-
 void CameraV3::State::syncGame(Serializer &s) {
 	syncVector(s, _usedCenter);
 	s.syncAsFloatLE(_scale);
@@ -338,24 +547,7 @@ void CameraV3::syncGame(Serializer &s) {
 	for (uint i = 0; i < kStateBackupCount; i++)
 		_backups[i].syncGame(s);
 
-	// originally the follow object is also searched for before changing the room
-	// so that would practically mean only the main characters could be reasonably found
-	// instead we fall back to global search
-	String name;
-	if (_followTarget != nullptr)
-		name = _followTarget->name();
-	s.syncString(name);
-	if (s.isLoading()) {
-		if (name.empty())
-			_followTarget = nullptr;
-		else {
-			_followTarget = dynamic_cast<WalkingCharacter *>(g_engine->world().getObjectByName(name.c_str()));
-			if (_followTarget == nullptr)
-				_followTarget = dynamic_cast<WalkingCharacter *>(g_engine->world().getObjectByNameFromAnyRoom(name.c_str()));
-			if (_followTarget == nullptr)
-				warning("Camera follow target from savestate was not found: %s", name.c_str());
-		}
-	}
+	syncFollowTarget(s, _followTarget);
 }
 
 struct CamLerpTask : public Task {
@@ -742,66 +934,4 @@ Task *CameraV3::shake(Process &process, Math::Vector2d amplitude, Math::Vector2d
 	return new CamShakeTask(process, amplitude, frequency, duration);
 }
 
-// The original name for this task is "disfraza" which I can only translate as "disguise"
-// It is a slightly bouncing vertical camera movement with fixed distance
-
-struct CamDisguiseTask final : public Task {
-	CamDisguiseTask(Process &process, int32 durationMs)
-		: Task(process)
-		, _camera(g_engine->cameraV3())
-		, _durationMs(durationMs) {}
-
-	CamDisguiseTask(Process &process, Serializer &s)
-		: Task(process)
-		, _camera(g_engine->cameraV3()) {
-		CamDisguiseTask::syncGame(s);
-	}
-
-	TaskReturn run() override {
-		if (_startTime == 0) {
-			_startPosition = { _camera._cur._usedCenter.x(), _camera._cur._usedCenter.y() };
-			_startTime = g_engine->getMillis();
-		}
-		if (_durationMs <= 0 || g_engine->getMillis() - _startTime >= (uint32)_durationMs)
-			return TaskReturn::finish(0);
-
-		Vector2d newPosition = _startPosition;
-		uint32 t = (g_engine->getMillis() - _startTime) * 5;
-		if (t <= 50)
-			newPosition.setY(_startPosition.getY() + t);
-		else if (t <= 150)
-			newPosition.setY(_startPosition.getY() - t + 100);
-		else if (t >= 200)
-			newPosition.setY(_startPosition.getY() + t - 200);
-		_camera.setPosition(newPosition);
-		_camera.setFollow(nullptr);
-
-		return TaskReturn::yield();
-	}
-
-	void debugPrint() override {
-		g_engine->getDebugger()->debugPrintf("\"Disguise\" camera for %dms", _durationMs);
-	}
-
-	void syncGame(Serializer &s) override {
-		Task::syncGame(s);
-		s.syncAsSint32LE(_durationMs);
-		s.syncAsUint32LE(_startTime);
-		syncVector(s, _startPosition);
-	}
-
-	const char *taskName() const override;
-
-private:
-	CameraV3 &_camera;
-	int32 _durationMs = 0;
-	uint32 _startTime = 0;
-	Vector2d _startPosition;
-};
-DECLARE_TASK(CamDisguiseTask)
-
-Task *CameraV3::disguise(Process &process, int32 duration) {
-	return new CamDisguiseTask(process, duration);
-}
-
 } // namespace Alcachofa
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 296d98d4db0..35d2c98ee4e 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -44,7 +44,7 @@ public:
 	virtual void preUpdate() = 0;
 	virtual void update() = 0;
 	virtual void setRoomBounds(Graphic &background) = 0;
-	virtual void setFollow(WalkingCharacter *target, bool catchUp = false) = 0;
+	virtual void setFollow(WalkingCharacter *target) = 0;
 	virtual void onChangedRoom(bool resetCamera) = 0;
 	virtual void onTriggeredDoor(WalkingCharacter *target) = 0;
 	virtual void onTriggeredDoor(Common::Point fixedPosition) = 0;
@@ -72,6 +72,37 @@ protected:
 		_mat2Dto3D;
 };
 
+class CameraV1 : public Camera {
+public:
+	Math::Angle rotation() const override;
+	float scale() const override;
+
+	void preUpdate() override;
+	void update() override;
+	void setRoomBounds(Graphic &background) override;
+	void setFollow(WalkingCharacter *target) override;
+	void onChangedRoom(bool resetCamera) override;
+	void onTriggeredDoor(WalkingCharacter *target) override;
+	void onTriggeredDoor(Common::Point fixedPosition) override;
+	void onScriptChangedCharacter(MainCharacterKind kind) override;
+	void onUserChangedCharacter() override;
+	void onOpenMenu() override;
+	void onCloseMenu() override;
+	void syncGame(Common::Serializer &s) override;
+	void lerpOrSet(Common::Point target, int32 mode);
+
+	Task *disguise(Process &process, int32 duration);
+
+private:
+	friend struct CamV1DisguiseTask;
+
+	WalkingCharacter *_followTarget = nullptr;
+	Math::Vector3d _target;
+	bool _isLerping = false;
+	float _lerpSpeed = 0.0f;
+	uint32 _lastUpdateTime = 0;
+};
+
 class CameraV3 : public Camera {
 public:
 	Math::Angle rotation() const override;
@@ -80,7 +111,8 @@ public:
 	void preUpdate() override;
 	void update() override;
 	void setRoomBounds(Graphic &background) override;
-	void setFollow(WalkingCharacter *target, bool catchUp = false) override;
+	void setFollow(WalkingCharacter *target) override;
+	void setFollow(WalkingCharacter *target, bool catchup);
 	void onChangedRoom(bool resetCamera) override;
 	void onTriggeredDoor(WalkingCharacter *target) override;
 	void onTriggeredDoor(Common::Point fixedPosition) override;
@@ -110,15 +142,8 @@ public:
 		int32 duration, EasingType moveEasingType, EasingType scaleEasingType);
 	Task *waitToStop(Process &process);
 	Task *shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration);
-	Task *disguise(Process &process, int32 duration);
 
 private:
-	void resetRotationAndScale();
-	void setPosition(Math::Vector2d v);
-	void setPosition(Math::Vector3d v);
-	void backup(uint slot);
-	void restore(uint slot);
-
 	friend struct CamLerpTask;
 	friend struct CamLerpPosTask;
 	friend struct CamLerpScaleTask;
@@ -127,7 +152,12 @@ private:
 	friend struct CamShakeTask;
 	friend struct CamWaitToStopTask;
 	friend struct CamSetInactiveAttributeTask;
-	friend struct CamDisguiseTask;
+
+	void resetRotationAndScale();
+	void setPosition(Math::Vector2d v);
+	void setPosition(Math::Vector3d v);
+	void backup(uint slot);
+	void restore(uint slot);
 	void updateFollowing(float deltaTime);
 
 	struct State {
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index ef0c4c85f5b..93296c4a163 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -924,7 +924,7 @@ void MainCharacter::walkTo(
 	}
 
 	WalkingCharacter::walkTo(target, endDirection, activateObject, activateAction);
-	if (this == g_engine->player().activeCharacter())
+	if (this == g_engine->player().activeCharacter() && g_engine->isV3())
 		g_engine->camera().setFollow(this);
 }
 
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 1c9d8aedd26..9e7c0383d5d 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -143,8 +143,8 @@ bool GlobalUI::updateChangingCharacter() {
 
 	player.setActiveCharacter(player.inactiveCharacter()->kind());
 	player.heldItem() = nullptr;
-	g_engine->camera().onUserChangedCharacter();
 	player.changeRoom(player.activeCharacter()->room()->name(), false);
+	g_engine->camera().onUserChangedCharacter();
 	g_engine->game().onUserChangedCharacter();
 
 	g_engine->sounds().startMusic(g_engine->game().getCharacterJingle(player.activeCharacterKind()));
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 7dbba1a9f1c..bab88b1e590 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -261,7 +261,6 @@ void Room::updateInteraction() {
 			player.activeCharacter()->room() == this &&
 			player.pressedObject() == nullptr) {
 			player.activeCharacter()->walkToMouse();
-			g_engine->camera().setFollow(player.activeCharacter());
 		}
 	} else {
 		player.selectedObject()->markSelected();
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 6da4d7e23d6..732f28353f6 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -938,7 +938,7 @@ private:
 			auto kind = getMainCharacterKindArg(0);
 			if (kind != MainCharacterKind::None)
 				target = &g_engine->world().getMainCharacterByKind(kind);
-			g_engine->camera().setFollow(target, getNumberArg(1) != 0);
+			g_engine->cameraV3().setFollow(target, getNumberArg(1) != 0);
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::CamShake:
@@ -1004,13 +1004,24 @@ private:
 				as3D(pointObject->position()), targetScale,
 				getNumberArg(2), (EasingType)getNumberArg(3), (EasingType)getNumberArg(4)));
 		}
+		case ScriptKernelTask::LerpOrSetCam: {
+			if (process().isActiveForPlayer()) {
+				auto pointObject = getObjectArg<PointObject>(0);
+				if (pointObject == nullptr)
+					pointObject = g_engine->game().unknownCamLerpTarget("LerpOrSetCam", getStringArg(0));
+				if (pointObject == nullptr)
+					return TaskReturn::finish(1);
+				g_engine->cameraV1().lerpOrSet(pointObject->position(), getNumberArg(1));
+			}
+			return TaskReturn::finish(0);
+		}
 		case ScriptKernelTask::Disguise: {
 			// a somewhat bouncy vertical camera movement used in V1
 			// or waiting for user to click
 			const auto duration = getNumberArg(0);
 			return TaskReturn::waitFor(duration == 0
 				? g_engine->input().waitForInput(process())
-				: g_engine->cameraV3().disguise(process(), duration));
+				: g_engine->cameraV1().disguise(process(), duration));
 		}
 
 		// Fades
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index e2595794fb1..6178d742549 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -119,6 +119,7 @@ enum class ScriptKernelTask {
 	LerpCamToObjectWithScale,
 	LerpCamToObjectResettingZ,
 	LerpCamRotation,
+	LerpOrSetCam, // only V1
 	FadeIn,
 	FadeOut,
 	FadeIn2,
diff --git a/engines/alcachofa/tasks.h b/engines/alcachofa/tasks.h
index b42a680103c..b2a3314eadf 100644
--- a/engines/alcachofa/tasks.h
+++ b/engines/alcachofa/tasks.h
@@ -38,7 +38,7 @@ DEFINE_TASK(CamLerpRotationTask)
 DEFINE_TASK(CamShakeTask)
 DEFINE_TASK(CamWaitToStopTask)
 DEFINE_TASK(CamSetInactiveAttributeTask)
-DEFINE_TASK(CamDisguiseTask)
+DEFINE_TASK(CamV1DisguiseTask)
 DEFINE_TASK(SayTextTask)
 DEFINE_TASK(AnimateCharacterTask)
 DEFINE_TASK(LerpLodBiasTask)




More information about the Scummvm-git-logs mailing list