[Scummvm-git-logs] scummvm master -> 6e9a340640686fe7dd95efbea34cbf3b7f4209af

peterkohaut peterkohaut at users.noreply.github.com
Sun Feb 4 17:35:26 CET 2018


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

Summary:
6e9a340640 BLADERUNNER: ESPER interface


Commit: 6e9a340640686fe7dd95efbea34cbf3b7f4209af
    https://github.com/scummvm/scummvm/commit/6e9a340640686fe7dd95efbea34cbf3b7f4209af
Author: Peter Kohaut (peter.kohaut at gmail.com)
Date: 2018-02-04T17:34:43+01:00

Commit Message:
BLADERUNNER: ESPER interface

Changed paths:
  A engines/bladerunner/ui/esper.cpp
  A engines/bladerunner/ui/esper.h
    engines/bladerunner/actor_clues.cpp
    engines/bladerunner/actor_walk.cpp
    engines/bladerunner/archive.cpp
    engines/bladerunner/bladerunner.cpp
    engines/bladerunner/bladerunner.h
    engines/bladerunner/combat.cpp
    engines/bladerunner/dialogue_menu.cpp
    engines/bladerunner/game_constants.h
    engines/bladerunner/game_flags.cpp
    engines/bladerunner/image.cpp
    engines/bladerunner/light.h
    engines/bladerunner/lights.h
    engines/bladerunner/module.mk
    engines/bladerunner/scene_objects.h
    engines/bladerunner/script/esper.cpp
    engines/bladerunner/script/esper.h
    engines/bladerunner/script/scene/rc01.cpp
    engines/bladerunner/script/script.cpp
    engines/bladerunner/script/script.h
    engines/bladerunner/set.h
    engines/bladerunner/shape.cpp
    engines/bladerunner/shape.h
    engines/bladerunner/text_resource.cpp
    engines/bladerunner/ui/elevator.cpp
    engines/bladerunner/ui/kia.cpp
    engines/bladerunner/ui/kia.h
    engines/bladerunner/ui/kia_section_crimes.cpp
    engines/bladerunner/ui/kia_section_suspects.cpp
    engines/bladerunner/ui/kia_shapes.cpp
    engines/bladerunner/ui/spinner.cpp
    engines/bladerunner/ui/ui_image_picker.cpp
    engines/bladerunner/ui/ui_image_picker.h
    engines/bladerunner/vqa_decoder.cpp
    engines/bladerunner/vqa_decoder.h
    engines/bladerunner/vqa_player.cpp
    engines/bladerunner/vqa_player.h
    engines/bladerunner/waypoints.h


diff --git a/engines/bladerunner/actor_clues.cpp b/engines/bladerunner/actor_clues.cpp
index 39fbc77..e1841fe 100644
--- a/engines/bladerunner/actor_clues.cpp
+++ b/engines/bladerunner/actor_clues.cpp
@@ -23,6 +23,7 @@
 #include "bladerunner/actor_clues.h"
 
 #include "bladerunner/bladerunner.h"
+#include "bladerunner/game_constants.h"
 #include "bladerunner/game_info.h"
 #include "bladerunner/crimes_database.h"
 
diff --git a/engines/bladerunner/actor_walk.cpp b/engines/bladerunner/actor_walk.cpp
index 7062422..fccc8e2 100644
--- a/engines/bladerunner/actor_walk.cpp
+++ b/engines/bladerunner/actor_walk.cpp
@@ -25,6 +25,7 @@
 #include "bladerunner/bladerunner.h"
 
 #include "bladerunner/actor.h"
+#include "bladerunner/game_constants.h"
 #include "bladerunner/game_info.h"
 #include "bladerunner/obstacles.h"
 #include "bladerunner/scene.h"
diff --git a/engines/bladerunner/archive.cpp b/engines/bladerunner/archive.cpp
index 8088e1a..6468acf 100644
--- a/engines/bladerunner/archive.cpp
+++ b/engines/bladerunner/archive.cpp
@@ -22,6 +22,8 @@
 
 #include "bladerunner/archive.h"
 
+#include "bladerunner/game_constants.h"
+
 #include "common/debug.h"
 
 namespace BladeRunner {
diff --git a/engines/bladerunner/bladerunner.cpp b/engines/bladerunner/bladerunner.cpp
index 266a8f9..efc8a4f 100644
--- a/engines/bladerunner/bladerunner.cpp
+++ b/engines/bladerunner/bladerunner.cpp
@@ -61,6 +61,7 @@
 #include "bladerunner/suspects_database.h"
 #include "bladerunner/text_resource.h"
 #include "bladerunner/ui/elevator.h"
+#include "bladerunner/ui/esper.h"
 #include "bladerunner/ui/kia.h"
 #include "bladerunner/ui/spinner.h"
 #include "bladerunner/vqa_decoder.h"
@@ -219,8 +220,6 @@ bool BladeRunnerEngine::startup(bool hasSavegames) {
 
 	_actorDialogueQueue = new ActorDialogueQueue(this);
 
-	// TODO: esper script
-
 	_settings = new Settings(this);
 
 	_itemPickup = new ItemPickup(this);
@@ -392,11 +391,11 @@ bool BladeRunnerEngine::startup(bool hasSavegames) {
 
 	for (int i = 0; i != 43; ++i) {
 		Shape *shape = new Shape(this);
-		shape->readFromContainer("SHAPES.SHP", i);
+		shape->open("SHAPES.SHP", i);
 		_shapes.push_back(shape);
 	}
 
-	// TODO: Esper
+	_esper = new ESPER(this);
 
 	// TODO: VK
 
@@ -467,7 +466,8 @@ void BladeRunnerEngine::shutdown() {
 
 	// TODO: Shutdown VK
 
-	// TODO: Shutdown Esper
+	delete _esper;
+	_esper = nullptr;
 
 	delete _mouse;
 	_mouse = nullptr;
@@ -736,7 +736,7 @@ void BladeRunnerEngine::gameTick() {
 		//probably not needed, this version of tick is just loading data from buffer
 		//_audioMixer->tick();
 
-		if (_kia->_currentSectionId) {
+		if (_kia->isOpen()) {
 			_kia->tick();
 			return;
 		}
@@ -747,7 +747,11 @@ void BladeRunnerEngine::gameTick() {
 			return;
 		}
 
-		// TODO: Esper
+		if (_esper->isOpen()) {
+			_esper->tick();
+			return;
+		}
+
 		// TODO: VK
 
 		if (_elevator->isOpen()) {
@@ -1031,32 +1035,101 @@ void BladeRunnerEngine::handleKeyUp(Common::Event &event) {
 		_speechSkipped = true;
 	}
 
-	// TODO(peterkohaut):
+	// TODO:
 	if (!playerHasControl() /*|| ActorInWalkingLoop*/) {
 		return;
 	}
 
-	if (_kia->_currentSectionId) {
+	if (_kia->isOpen()) {
 		_kia->handleKeyUp(event.kbd);
 		return;
 	}
 
-	if (event.kbd.keycode == Common::KEYCODE_TAB) {
-		_kia->openLastOpened();
-	} else if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
-		_kia->openOptions();
-	} else if (event.kbd.keycode == Common::KEYCODE_SPACE) {
-		// TODO(peterkohaut):
-		// combat::switchCombatMode(&Combat);
+	if (_spinner->isOpen()) {
+		return;
+	}
+
+	if (_elevator->isOpen()) {
+		return;
+	}
+
+	if (_esper->isOpen()) {
+		return;
+	}
+
+	if (_dialogueMenu->isOpen()) {
+		return;
+	}
+
+	//TODO: scores
+	switch (event.kbd.keycode) {
+		case Common::KEYCODE_TAB:
+			_kia->openLastOpened();
+			break;
+		case Common::KEYCODE_ESCAPE:
+			_kia->open(kKIASectionSettings);
+			break;
+		case Common::KEYCODE_SPACE:
+			// TODO: combat::switchCombatMode(&Combat);
+			break;
+		default:
+			break;
 	}
 }
 
 void BladeRunnerEngine::handleKeyDown(Common::Event &event) {
-	// if ( PlayerHasControl <= 0 && ActorWalkingLoop != 1 && PlayingSpeechLine != 1 && VqaIsPlaying != 1 ) {
-	if (_kia->_currentSectionId) {
+	//TODO:
+	if (!playerHasControl() /* || ActorWalkingLoop || PlayingSpeechLine || VqaIsPlaying */) {
+		return;
+	}
+
+	if (_kia->isOpen()) {
 		_kia->handleKeyDown(event.kbd);
 	}
-	// }
+
+	if (_spinner->isOpen()) {
+		return;
+	}
+
+	if (_elevator->isOpen()) {
+		return;
+	}
+
+	if (_esper->isOpen()) {
+		return;
+	}
+
+	if (_dialogueMenu->isOpen()) {
+		return;
+	}
+
+	//TODO: scores
+
+	switch (event.kbd.keycode) {
+		case Common::KEYCODE_F1:
+			_kia->open(kKIASectionHelp);
+			break;
+		case Common::KEYCODE_F2:
+			_kia->open(kKIASectionSave);
+			break;
+		case Common::KEYCODE_F3:
+			_kia->open(kKIASectionLoad);
+			break;
+		case Common::KEYCODE_F4:
+			_kia->open(kKIASectionCrimes);
+			break;
+		case Common::KEYCODE_F5:
+			_kia->open(kKIASectionSuspects);
+			break;
+		case Common::KEYCODE_F6:
+			_kia->open(kKIASectionClues);
+			break;
+		case Common::KEYCODE_F10:
+			_kia->open(kKIASectionQuit);
+			break;
+		default:
+			break;
+	}
 }
 
 void BladeRunnerEngine::handleMouseAction(int x, int y, bool buttonLeft, bool buttonDown) {
@@ -1064,7 +1137,7 @@ void BladeRunnerEngine::handleMouseAction(int x, int y, bool buttonLeft, bool bu
 		return;
 	}
 
-	if (_kia->_currentSectionId) {
+	if (_kia->isOpen()) {
 		if (buttonDown) {
 			_kia->handleMouseDown(x, y, buttonLeft);
 		} else {
@@ -1082,6 +1155,15 @@ void BladeRunnerEngine::handleMouseAction(int x, int y, bool buttonLeft, bool bu
 		return;
 	}
 
+	if (_esper->isOpen()) {
+		if (buttonDown) {
+			_esper->handleMouseDown(x, y, buttonLeft);
+		} else {
+			_esper->handleMouseUp(x, y, buttonLeft);
+		}
+		return;
+	}
+
 	if (_elevator->isOpen()) {
 		if (buttonDown) {
 			_elevator->handleMouseDown(x, y);
diff --git a/engines/bladerunner/bladerunner.h b/engines/bladerunner/bladerunner.h
index 593f414..ba47f2c 100644
--- a/engines/bladerunner/bladerunner.h
+++ b/engines/bladerunner/bladerunner.h
@@ -33,11 +33,6 @@
 
 #include "graphics/surface.h"
 
-// remove these when game is playable
-#define BLADERUNNER_DEBUG_RENDERING 0
-#define BLADERUNNER_DEBUG_CONSOLE 0
-#define BLADERUNNER_DEBUG_GAME 0
-
 namespace Common {
 struct Event;
 }
@@ -46,22 +41,6 @@ struct ADGameDescription;
 
 namespace BladeRunner {
 
-enum AnimationModes {
-	kAnimationModeIdle = 0,
-	kAnimationModeWalk = 1,
-	kAnimationModeRun = 2,
-	kAnimationModeCombatIdle = 4,
-	kAnimationModeCombatWalk = 7,
-	kAnimationModeCombatRun = 8
-};
-
-enum SceneLoopMode {
-	kSceneLoopModeLoseControl = 0,
-	kSceneLoopModeChangeSet = 1,
-	kSceneLoopMode2 = 2,
-	kSceneLoopModeSpinner = 3
-};
-
 class Actor;
 class ActorDialogueQueue;
 class ScreenEffects;
@@ -75,6 +54,7 @@ class CrimesDatabase;
 class Combat;
 class DialogueMenu;
 class Elevator;
+class ESPER;
 class Font;
 class GameFlags;
 class GameInfo;
@@ -125,6 +105,7 @@ public:
 	Combat             *_combat;
 	DialogueMenu       *_dialogueMenu;
 	Elevator           *_elevator;
+	ESPER              *_esper;
 	GameFlags          *_gameFlags;
 	GameInfo           *_gameInfo;
 	ItemPickup         *_itemPickup;
diff --git a/engines/bladerunner/combat.cpp b/engines/bladerunner/combat.cpp
index 79491e7..26b8b5d 100644
--- a/engines/bladerunner/combat.cpp
+++ b/engines/bladerunner/combat.cpp
@@ -25,6 +25,7 @@
 
 #include "bladerunner/actor.h"
 #include "bladerunner/bladerunner.h"
+#include "bladerunner/game_constants.h"
 #include "bladerunner/settings.h"
 
 namespace BladeRunner {
diff --git a/engines/bladerunner/dialogue_menu.cpp b/engines/bladerunner/dialogue_menu.cpp
index d23d8e0..9f43f55 100644
--- a/engines/bladerunner/dialogue_menu.cpp
+++ b/engines/bladerunner/dialogue_menu.cpp
@@ -42,7 +42,7 @@ DialogueMenu::DialogueMenu(BladeRunnerEngine *vm) {
 	_shapes.reserve(8);
 	for (int i = 0; i != 8; ++i) {
 		_shapes.push_back(Shape(_vm));
-		bool r = _shapes[i].readFromContainer("DIALOG.SHP", i);
+		bool r = _shapes[i].open("DIALOG.SHP", i);
 		assert(r);
 		(void)r;
 	}
diff --git a/engines/bladerunner/game_constants.h b/engines/bladerunner/game_constants.h
index 9915bc9..bf7e101 100644
--- a/engines/bladerunner/game_constants.h
+++ b/engines/bladerunner/game_constants.h
@@ -23,6 +23,11 @@
 #ifndef BLADERUNNER_GAME_CONSTANTS_H
 #define BLADERUNNER_GAME_CONSTANTS_H
 
+//TODO: remove these when game is playable
+#define BLADERUNNER_DEBUG_RENDERING 0
+#define BLADERUNNER_DEBUG_CONSOLE 0
+#define BLADERUNNER_DEBUG_GAME 0
+
 namespace BladeRunner {
 
 enum Actors {
@@ -471,6 +476,23 @@ enum Outtakes {
 	kOuttakeBladeRunner = 41
 };
 
+enum AnimationModes {
+	kAnimationModeIdle = 0,
+	kAnimationModeWalk = 1,
+	kAnimationModeRun = 2,
+	kAnimationModeCombatIdle = 4,
+	kAnimationModeCombatWalk = 7,
+	kAnimationModeCombatRun = 8
+};
+
+enum SceneLoopMode {
+	kSceneLoopModeLoseControl = 0,
+	kSceneLoopModeChangeSet = 1,
+	kSceneLoopMode2 = 2,
+	kSceneLoopModeSpinner = 3
+};
+
+
 } // End of namespace BladeRunner
 
 #endif
diff --git a/engines/bladerunner/game_flags.cpp b/engines/bladerunner/game_flags.cpp
index 81fe6a0..f6ae7b1 100644
--- a/engines/bladerunner/game_flags.cpp
+++ b/engines/bladerunner/game_flags.cpp
@@ -22,6 +22,8 @@
 
 #include "bladerunner/game_flags.h"
 
+#include "bladerunner/game_constants.h"
+
 #include "common/debug.h"
 
 namespace BladeRunner {
diff --git a/engines/bladerunner/image.cpp b/engines/bladerunner/image.cpp
index cc11e5b..925b026 100644
--- a/engines/bladerunner/image.cpp
+++ b/engines/bladerunner/image.cpp
@@ -23,7 +23,6 @@
 #include "bladerunner/image.h"
 
 #include "bladerunner/bladerunner.h"
-
 #include "bladerunner/decompress_lcw.h"
 
 #include "common/rect.h"
diff --git a/engines/bladerunner/light.h b/engines/bladerunner/light.h
index 8ad8636..03be064 100644
--- a/engines/bladerunner/light.h
+++ b/engines/bladerunner/light.h
@@ -37,9 +37,7 @@ namespace BladeRunner {
 class Lights;
 
 class Light {
-#if BLADERUNNER_DEBUG_RENDERING
 	friend class BladeRunnerEngine;
-#endif
 	friend class Lights;
 	friend class SliceRenderer;
 
diff --git a/engines/bladerunner/lights.h b/engines/bladerunner/lights.h
index 904a96f..ee89fbb 100644
--- a/engines/bladerunner/lights.h
+++ b/engines/bladerunner/lights.h
@@ -32,9 +32,7 @@
 namespace BladeRunner {
 
 class Lights {
-#if BLADERUNNER_DEBUG_RENDERING
 	friend class BladeRunnerEngine;
-#endif
 	friend class SliceRendererLights;
 
 	BladeRunnerEngine *_vm;
diff --git a/engines/bladerunner/module.mk b/engines/bladerunner/module.mk
index 51251ee..3b461d5 100644
--- a/engines/bladerunner/module.mk
+++ b/engines/bladerunner/module.mk
@@ -176,6 +176,7 @@ MODULE_OBJS = \
 	suspects_database.o \
 	text_resource.o \
 	ui/elevator.o \
+	ui/esper.o \
 	ui/kia.o \
 	ui/kia_log.o \
 	ui/kia_section_base.o \
diff --git a/engines/bladerunner/scene_objects.h b/engines/bladerunner/scene_objects.h
index b873980..f6aa528 100644
--- a/engines/bladerunner/scene_objects.h
+++ b/engines/bladerunner/scene_objects.h
@@ -46,9 +46,7 @@ enum SceneObjectOffset {
 };
 
 class SceneObjects {
-#if BLADERUNNER_DEBUG_RENDERING
 	friend class BladeRunnerEngine;
-#endif
 	static const int kSceneObjectCount = 115;
 
 	struct SceneObject {
diff --git a/engines/bladerunner/script/esper.cpp b/engines/bladerunner/script/esper.cpp
index ca779d6..e6c0390 100644
--- a/engines/bladerunner/script/esper.cpp
+++ b/engines/bladerunner/script/esper.cpp
@@ -23,9 +23,28 @@
 #include "bladerunner/script/esper.h"
 
 #include "bladerunner/bladerunner.h"
+#include "bladerunner/mouse.h"
 
 namespace BladeRunner {
 
+void ESPERScript::initialize() {
+	_vm->_mouse->disable();
+	SCRIPT_ESPER_DLL_Initialize();
+	_vm->_mouse->enable();
+}
+
+void ESPERScript::photoSelected(int photoId) {
+	_vm->_mouse->disable();
+	SCRIPT_ESPER_DLL_Photo_Selected(photoId);
+	_vm->_mouse->enable();
+}
+
+void ESPERScript::specialRegionSelected(int photoId, int regionId) {
+	_vm->_mouse->disable();
+	SCRIPT_ESPER_DLL_Special_Region_Selected(photoId, regionId);
+	_vm->_mouse->enable();
+}
+
 void ESPERScript::SCRIPT_ESPER_DLL_Initialize() {
 	int v0 = 0;
 	if (Actor_Clue_Query(kActorMcCoy, kClueRuncitersVideo)) {
@@ -39,7 +58,7 @@ void ESPERScript::SCRIPT_ESPER_DLL_Initialize() {
 		if (!Actor_Clue_Query(kActorMcCoy, kClueRuncitersViewB)) {
 			Actor_Clue_Acquire(kActorMcCoy, kClueRuncitersViewB, 1, kActorRunciter);
 		}
-		ESPER_Add_Photo("RC02_FA.IMG", 1, 1);
+		ESPER_Add_Photo("RC02_RA.IMG", 1, 1);
 	}
 	if (Actor_Clue_Query(kActorMcCoy, kClueEarlyQsClub)) {
 		if (!Actor_Clue_Query(kActorMcCoy, kClueOuterDressingRoom)) {
diff --git a/engines/bladerunner/script/esper.h b/engines/bladerunner/script/esper.h
index 683ecf6..63e30e5 100644
--- a/engines/bladerunner/script/esper.h
+++ b/engines/bladerunner/script/esper.h
@@ -31,10 +31,13 @@ class BladeRunnerEngine;
 
 class ESPERScript : ScriptBase {
 public:
-	ESPERScript(BladeRunnerEngine *vm)
-		: ScriptBase(vm) {
-	}
+	ESPERScript(BladeRunnerEngine *vm) : ScriptBase(vm) {}
 
+	void initialize();
+	void photoSelected(int photoId);
+	void specialRegionSelected(int photoId, int regionId);
+
+private:
 	void SCRIPT_ESPER_DLL_Initialize();
 	void SCRIPT_ESPER_DLL_Photo_Selected(int photo);
 	bool SCRIPT_ESPER_DLL_Special_Region_Selected(int photo, int region);
diff --git a/engines/bladerunner/script/scene/rc01.cpp b/engines/bladerunner/script/scene/rc01.cpp
index dd793ad..347f0e8 100644
--- a/engines/bladerunner/script/scene/rc01.cpp
+++ b/engines/bladerunner/script/scene/rc01.cpp
@@ -30,8 +30,9 @@ void SceneScriptRC01::InitializeScene() {
 	Game_Flag_Set(kFlagIntroPlayed); // force skip intro
 	Game_Flag_Set(kFlagRC02toRC01); // no landing
 	// Game_Flag_Set(kFlagRC01PoliceDone);
-	// Game_Flag_Set(249);
-	Game_Flag_Set(kFlagKIAPrivacyAddon);
+	// Game_Flag_Set(kFlagKIAPrivacyAddon);
+
+	// ESPER_Flag_To_Activate();
 #endif
 
 	if (!Game_Flag_Query(kFlagIntroPlayed)) {
diff --git a/engines/bladerunner/script/script.cpp b/engines/bladerunner/script/script.cpp
index 91a755b..3c0c8e1 100644
--- a/engines/bladerunner/script/script.cpp
+++ b/engines/bladerunner/script/script.cpp
@@ -50,6 +50,7 @@
 #include "bladerunner/suspects_database.h"
 #include "bladerunner/text_resource.h"
 #include "bladerunner/ui/elevator.h"
+#include "bladerunner/ui/esper.h"
 #include "bladerunner/ui/kia.h"
 #include "bladerunner/ui/spinner.h"
 #include "bladerunner/vector.h"
@@ -1076,8 +1077,12 @@ int ScriptBase::Spinner_Interface_Choose_Dest(int loopId, bool immediately) {
 }
 
 void ScriptBase::ESPER_Flag_To_Activate() {
-	//TODO
-	warning("ESPER_Flag_To_Activate()");
+	if (!_vm->_esper->isOpen()) {
+		_vm->_esper->open(&_vm->_surfaceBack);
+		while (_vm->_esper->isOpen()) {
+			_vm->gameTick();
+		}
+	}
 }
 
 bool ScriptBase::Voight_Kampff_Activate(int a1, int a2){
@@ -1282,14 +1287,12 @@ void ScriptBase::KIA_Play_Photograph(int photographId) {
 	_vm->_kia->playPhotograph(photographId);
 }
 
-void ScriptBase::ESPER_Add_Photo(const char *fileName, int a2, int a3) {
-	//TODO
-	warning("ESPER_Add_Photo(%s, %d, %d)", fileName, a2, a3);
+void ScriptBase::ESPER_Add_Photo(const char *name, int photoId, int shapeId) {
+	_vm->_esper->addPhoto(name, photoId, shapeId);
 }
 
-void ScriptBase::ESPER_Define_Special_Region(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, int a11, int a12, int a13, const char *name) {
-	//TODO
-	warning("ESPER_Define_Special_Region(%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %s)",  a1,  a2,  a3,  a4,  a5,  a6,  a7,  a8,  a9,  a10,  a11,  a12,  a13, name);
+void ScriptBase::ESPER_Define_Special_Region(int regionId, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, int a11, int a12, int a13, const char *name) {
+	_vm->_esper->defineRegion(regionId, Common::Rect(a2, a3, a4, a5), Common::Rect(a6, a7, a8, a9), Common::Rect(a10, a11, a12, a13), name);
 }
 
 void ScriptBase::VK_Add_Question(int a1, int a2, int a3) {
diff --git a/engines/bladerunner/script/script.h b/engines/bladerunner/script/script.h
index dabdec6..574f3a8 100644
--- a/engines/bladerunner/script/script.h
+++ b/engines/bladerunner/script/script.h
@@ -272,7 +272,7 @@ protected:
 	void AI_Movement_Track_Append(int actorId, int waypointId, int delay);
 	void AI_Movement_Track_Flush(int actorId);
 
-	void ESPER_Add_Photo(const char *fileName, int a2, int a3);
+	void ESPER_Add_Photo(const char *name, int photoId, int shapeId);
 	void ESPER_Define_Special_Region(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, int a11, int a12, int a13, const char *name);
 
 	void KIA_Play_Actor_Dialogue(int actorId, int sentenceId);
diff --git a/engines/bladerunner/set.h b/engines/bladerunner/set.h
index 17d0626..d888bff 100644
--- a/engines/bladerunner/set.h
+++ b/engines/bladerunner/set.h
@@ -37,9 +37,8 @@ class SetEffects;
 class SceneObjects;
 
 class Set {
-#if BLADERUNNER_DEBUG_RENDERING
 	friend class BladeRunnerEngine;
-#endif
+
 	struct Object {
 		char        name[20];
 		BoundingBox bbox;
diff --git a/engines/bladerunner/shape.cpp b/engines/bladerunner/shape.cpp
index f5a6804..4584344 100644
--- a/engines/bladerunner/shape.cpp
+++ b/engines/bladerunner/shape.cpp
@@ -43,16 +43,16 @@ Shape::~Shape() {
 	delete[] _data;
 }
 
-bool Shape::readFromContainer(const Common::String &container, int index) {
+bool Shape::open(const Common::String &container, int index) {
 	Common::ScopedPtr<Common::SeekableReadStream> stream(_vm->getResourceStream(container));
 	if (!stream) {
-		debug("Shape::readFromContainer failed to open '%s'", container.c_str());
+		debug("Shape::open failed to open '%s'", container.c_str());
 		return false;
 	}
 
 	uint32 count = stream->readUint32LE();
 	if (index < 0 || (uint32)index >= count) {
-		debug("Shape::readFromContainer invalid index %d (count %u)", index, count);
+		debug("Shape::open invalid index %d (count %u)", index, count);
 		return false;
 	}
 
@@ -63,7 +63,7 @@ bool Shape::readFromContainer(const Common::String &container, int index) {
 		size   = stream->readUint32LE();
 
 		if (size != width * height * 2) {
-			debug("Shape::readFromContainer size mismatch (w %d, h %d, sz %d)", width, height, size);
+			debug("Shape::open size mismatch (w %d, h %d, sz %d)", width, height, size);
 			return false;
 		}
 
@@ -74,7 +74,7 @@ bool Shape::readFromContainer(const Common::String &container, int index) {
 
 	// Enfoce a reasonable size limit
 	if (width >= 2048 || height >= 2048) {
-		warning("Shape::readFromContainer shape too big (%d, %d)", width, height);
+		warning("Shape::open shape too big (%d, %d)", width, height);
 	}
 
 	_width  = width;
@@ -82,7 +82,7 @@ bool Shape::readFromContainer(const Common::String &container, int index) {
 	_data   = new byte[size];
 
 	if (stream->read(_data, size) != size) {
-		debug("Shape::readFromContainer error reading shape %d (w %d, h %d, sz %d)", index, width, height, size);
+		debug("Shape::open error reading shape %d (w %d, h %d, sz %d)", index, width, height, size);
 		return false;
 	}
 
diff --git a/engines/bladerunner/shape.h b/engines/bladerunner/shape.h
index 799826c..fa286e5 100644
--- a/engines/bladerunner/shape.h
+++ b/engines/bladerunner/shape.h
@@ -44,7 +44,7 @@ public:
 	Shape(BladeRunnerEngine *vm);
 	~Shape();
 
-	bool readFromContainer(const Common::String &container, int index);
+	bool open(const Common::String &container, int index);
 
 	void draw(Graphics::Surface &surface, int x, int y) const;
 
diff --git a/engines/bladerunner/text_resource.cpp b/engines/bladerunner/text_resource.cpp
index 42d3003..ec5706e 100644
--- a/engines/bladerunner/text_resource.cpp
+++ b/engines/bladerunner/text_resource.cpp
@@ -20,10 +20,10 @@
  *
  */
 
-
 #include "bladerunner/text_resource.h"
 
 #include "bladerunner/bladerunner.h"
+#include "bladerunner/game_constants.h"
 
 #include "common/debug.h"
 #include "common/util.h"
diff --git a/engines/bladerunner/ui/elevator.cpp b/engines/bladerunner/ui/elevator.cpp
index 1ee4115..4c72be2 100644
--- a/engines/bladerunner/ui/elevator.cpp
+++ b/engines/bladerunner/ui/elevator.cpp
@@ -76,7 +76,7 @@ int Elevator::activate(int elevatorId) {
 
 	for (int i = 0; i != 16; ++i) {
 		_shapes.push_back(new Shape(_vm));
-		_shapes[i]->readFromContainer("ELEVATOR.SHP", i);
+		_shapes[i]->open("ELEVATOR.SHP", i);
 	}
 
 	_imagePicker->resetImages();
diff --git a/engines/bladerunner/ui/esper.cpp b/engines/bladerunner/ui/esper.cpp
new file mode 100644
index 0000000..ea1df6b
--- /dev/null
+++ b/engines/bladerunner/ui/esper.cpp
@@ -0,0 +1,1791 @@
+/* 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 2
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include "bladerunner/ui/esper.h"
+
+#include "bladerunner/actor.h"
+#include "bladerunner/ambient_sounds.h"
+#include "bladerunner/audio_player.h"
+#include "bladerunner/bladerunner.h"
+#include "bladerunner/decompress_lcw.h"
+#include "bladerunner/font.h"
+#include "bladerunner/game_info.h"
+#include "bladerunner/game_constants.h"
+#include "bladerunner/mouse.h"
+#include "bladerunner/shape.h"
+#include "bladerunner/script/esper.h"
+#include "bladerunner/text_resource.h"
+#include "bladerunner/ui/ui_image_picker.h"
+#include "bladerunner/vqa_player.h"
+
+#include "common/rect.h"
+#include "common/str.h"
+
+#include "graphics/surface.h"
+
+namespace BladeRunner {
+
+const Common::Rect ESPER::kScreen = Common::Rect(135, 123, 435, 387);
+
+ESPER::ESPER(BladeRunnerEngine *vm) {
+	_vm = vm;
+
+	_isWaiting          = false;
+	_shapeButton        = nullptr;
+	_shapeThumbnail     = nullptr;
+	_regionSelectedAck  = false;
+	_isDrawingSelection = false;
+
+	_isOpen         = false;
+	_photoData      = nullptr;
+	_viewportData   = nullptr;
+	_shapeButton    = nullptr;
+	_shapeThumbnail = nullptr;
+	_vqaMainPlayer  = nullptr;
+	_vqaPhotoPlayer = nullptr;
+	_script         = nullptr;
+
+	reset();
+	_buttons = new UIImagePicker(vm, 16);
+}
+
+ESPER::~ESPER() {
+	delete _buttons;
+	reset();
+}
+
+void ESPER::open(Graphics::Surface *surface) {
+	// CD-changing logic has been removed
+
+	while (!_vm->playerHasControl()) {
+		_vm->playerGainsControl();
+	}
+
+	while (_vm->_mouse->isDisabled()) {
+		_vm->_mouse->enable();
+	}
+
+	//TODO: time->lock()
+	_ambientVolume = _vm->_ambientSounds->getVolume();
+	_vm->_ambientSounds->setVolume(_ambientVolume / 2);
+
+	reset();
+
+	if (!_vm->openArchive("MODE.MIX")) {
+		return;
+	}
+
+	_photoData = new Graphics::Surface();
+	_photoData->create(kPhotoWidth, kPhotoHeight, createRGB555());
+
+	_viewportData = new Graphics::Surface();
+	_viewportData->create(kScreen.width(), kScreen.height(), createRGB555());
+
+	_viewportNext = _viewport;
+
+	_vm->_mainFont->setColor(0x001F);
+
+	_shapeButton = new Shape(_vm);
+	if (!_shapeButton->open("ESPBUTTN.SHP", 0)) {
+		return;
+	}
+
+	_shapesPhotos.resize(10);
+
+	_vqaMainPlayer = new VQAPlayer(_vm, &_vm->_surfaceBack);
+	if (!_vqaMainPlayer->open("ESPER.VQA")) {
+		return;
+	}
+	_vqaMainPlayer->setLoop(2, -1, kLoopSetModeJustStart, nullptr, nullptr);
+
+	_isOpen = true;
+	_flash = false;
+
+	_script = new ESPERScript(_vm);
+	activate(true);
+}
+
+void ESPER::close() {
+	// CD-changing logic has been removed
+	delete _script;
+	_script = nullptr;
+
+	_vm->_audioPlayer->playAud(_vm->_gameInfo->getSfxTrack(425), 25, 0, 0, 50, 0);
+
+	unloadPhotos();
+	_shapesPhotos.clear();
+
+	delete _shapeThumbnail;
+	_shapeThumbnail = nullptr;
+
+	_buttons->deactivate();
+	_buttons->resetImages();
+
+	delete _shapeButton;
+	_shapeButton = nullptr;
+
+	delete _photoData;
+	_photoData = nullptr;
+
+	delete _viewportData;
+	_viewportData = nullptr;
+
+	if (_vqaMainPlayer) {
+		_vqaMainPlayer->close();
+		delete _vqaMainPlayer;
+		_vqaMainPlayer= nullptr;
+	}
+
+	_vm->closeArchive("MODE.MIX");
+
+	//TODO: time->unlock()
+	_vm->_ambientSounds->setVolume(_ambientVolume);
+	//TODO: _vm->_scene->resume(false);
+	reset();
+}
+
+bool ESPER::isOpen() {
+	return _isOpen;
+}
+
+void ESPER::handleMouseUp(int x, int y, bool mainButton) {
+	bool actionHandled = _buttons->handleMouseAction(x, y, false, true, false);
+	if (mainButton) {
+		_isMouseDown = false;
+		if (!actionHandled) {
+			if (_isScrolling) {
+				scrollingStop();
+			} else if (_isDrawingSelection && _mouseOverScroll == 4) {
+				_isDrawingSelection = false;
+				resetSelectionRect();
+			}
+		}
+	} else if (_statePhoto == kEsperPhotoStatePhotoZoomOut) {
+		zoomOutStop();
+	}
+}
+
+void ESPER::handleMouseDown(int x, int y, bool mainButton) {
+	bool actionHandled = _buttons->handleMouseAction(x, y, true, false, false);
+
+	if (actionHandled || _vm->_mouse->isDisabled()) {
+		return;
+	}
+
+	if (mainButton) {
+		if (_statePhoto != kEsperPhotoStateVideoZoomOut) {
+			if (kScreen.contains(x, y)) {
+				_isMouseDown = true;
+				playSound(460, 100);
+			}
+			if ( _mouseOverScroll >= 0 && _mouseOverScroll <= 3 && !_isScrolling) {
+				scrollingStart(_mouseOverScroll);
+			}
+			tick();
+		}
+	} else {
+		if (_statePhoto == kEsperPhotoStateShow || _statePhoto == kEsperPhotoStateVideoShow) {
+			zoomOutStart();
+		}
+	}
+}
+
+void ESPER::tick() {
+	if (!_vm->_windowIsActive) {
+		return;
+	}
+
+	tickSound();
+
+	blit(_vm->_surfaceBack, _vm->_surfaceFront);
+
+	int mouseX, mouseY;
+	_vm->_mouse->getXY(&mouseX, &mouseY);
+	if (!_vm->_mouse->isDisabled()) {
+		_buttons->handleMouseAction(mouseX, mouseY, false, false, false);
+	}
+
+	if (!_isOpen) {
+		return;
+	}
+
+	draw(_vm->_surfaceFront);
+	_buttons->draw(_vm->_surfaceFront);
+	tickMouse(_vm->_surfaceFront);
+	tickSound();
+
+	_vm->blitToScreen(_vm->_surfaceFront);
+
+	// TODO: implement 60hz lock for smoother experience
+	_vm->_system->delayMillis(10);
+
+	if (_statePhoto == kEsperPhotoStateVideoShow) {
+		if (_regionSelectedAck)	{
+			_regionSelectedAck = false;
+			_script->specialRegionSelected(_photoIdSelected, _regions[_regionSelected].regionId);
+		}
+	}
+}
+
+void ESPER::resume() {
+	//TODO
+}
+
+void ESPER::addPhoto(const char *name, int photoId, int shapeId) {
+	int i = findEmptyPhoto();
+	if (i >= 0) {
+		_photos[i].shapeId = shapeId;
+		_photos[i].isPresent = true;
+		_photos[i].photoId = photoId;
+		strcpy(_photos[i].name, name);
+
+		assert((uint)shapeId < _shapesPhotos.size());
+		_shapesPhotos[shapeId] = new Shape(_vm);
+		_shapesPhotos[shapeId]->open("ESPTHUMB.SHP", shapeId);
+
+		_buttons->defineImage(i,
+			Common::Rect(
+				100 * (i % 3) + kScreen.left + 3,
+				 66 * (i / 3) + kScreen.top  + 3,
+				100 * (i % 3) + kScreen.left + 100 - 3,
+				 66 * (i / 3) + kScreen.top  + 66  - 3
+			),
+			_shapesPhotos[shapeId],
+			_shapesPhotos[shapeId],
+			_shapesPhotos[shapeId],
+			nullptr);
+	}
+	playSound(420, 25);
+	wait(300);
+	tick();
+}
+
+void ESPER::defineRegion(int regionId, Common::Rect inner, Common::Rect outer, Common::Rect selection, const char *name) {
+	int i = findEmptyRegion();
+	if (i >= 0) {
+		_regions[i].isPresent = true;
+		_regions[i].regionId = regionId;
+		_regions[i].rectInner = inner;
+		_regions[i].rectOuter = outer;
+		_regions[i].rectSelection = selection;
+		strncpy(_regions[i].name, name, 13);
+		_regions[i].name[13] = 0;
+	}
+}
+
+void ESPER::mouseDownCallback(int buttonId, void *callbackData) {
+	ESPER *self = ((ESPER *)callbackData);
+	if (self->_statePhoto != kEsperPhotoStateVideoZoomOut && buttonId == kPhotoCount + 2) {
+		self->zoomOutStart();
+	}
+}
+
+void ESPER::mouseUpCallback(int buttonId, void *callbackData) {
+	ESPER *self = (ESPER *)callbackData;
+	if (buttonId < kPhotoCount) {
+		self->selectPhoto(buttonId);
+	} else if (self->_statePhoto != kEsperPhotoStateVideoZoomOut) {
+		if (buttonId == kPhotoCount + 1) {
+			// TODO: is it even used?
+		} else if (buttonId == kPhotoCount + 2) {
+			self->zoomOutStop();
+		} else if (buttonId == kPhotoCount + 3) {
+			self->goBack();
+		}
+	}
+}
+
+void ESPER::reset() {
+	delete _photoData;
+	_photoData = nullptr;
+
+	delete _viewportData;
+	_viewportData = nullptr;
+
+	delete _shapeButton;
+	_shapeButton = nullptr;
+
+	delete _shapeThumbnail;
+	_shapeThumbnail = nullptr;
+
+	delete _vqaMainPlayer;
+	_vqaMainPlayer = nullptr;
+
+	delete _vqaPhotoPlayer;
+	_vqaPhotoPlayer = nullptr;
+
+	delete _script;
+	_script = nullptr;
+
+	_isOpen = false;
+
+	_shapesPhotos.clear();
+	resetData();
+}
+
+void ESPER::resetData() {
+	if (_vqaPhotoPlayer) {
+		_vqaPhotoPlayer->close();
+		delete _vqaPhotoPlayer;
+		_vqaPhotoPlayer = nullptr;
+	}
+	if (_shapeThumbnail) {
+		delete _shapeThumbnail;
+		_shapeThumbnail = nullptr;
+	}
+
+	_viewport.left = 0;
+	_viewport.top = 0;
+
+	_regionSelectedAck = false;
+	_mouseOverScroll = -1;
+	_isMouseDown = 0;
+	_viewportNext.left = 0;
+	_viewportNext.top = 0;
+
+	_selectionRect.left = -1;
+	_selectionRect.top = -1;
+	_selectionRect.right = -1;
+	_selectionRect.bottom = -1;
+
+	_selectionCrosshairX = -1;
+	_selectionCrosshairY = -1;
+
+	_stateMain = kEsperMainStatePhoto;
+	_statePhoto = kEsperPhotoStateShow;
+
+	_isDrawingSelection = false;
+	_flash = false;
+	_isScrolling = false;
+
+	_scrollingDirection = -1;
+	_timeScrollNext = 0;
+
+	resetPhotos();
+	resetRegions();
+	resetViewport();
+	resetSelectionBlinking();
+	prepareZoom();
+	resetPhotoZooming();
+	resetPhotoOpening();
+
+	_soundId1 = -1;
+	_soundId2 = -1;
+	_soundId3 = -1;
+}
+
+void ESPER::resetPhotos() {
+	for (int i = 0; i < kPhotoCount; ++i) {
+		_photos[i].isPresent = false;
+		_photos[i].photoId = -1;
+	}
+}
+
+void ESPER::resetRegions() {
+	for (int i = 0; i < kRegionCount; ++i) {
+		_regions[i].isPresent = false;
+		_regions[i].regionId = -1;
+	}
+}
+
+void ESPER::resetViewport() {
+	_zoomHorizontal = (float)(kScreen.width()) / (float)kPhotoWidth;
+	_zoomVertical   = (float)(kScreen.height()) / (float)kPhotoHeight;
+	_zoom           = _zoomVertical;
+	_zoomMin        = _zoom;
+
+	_timeZoomOutNext = 0;
+	_viewportPositionX = kPhotoWidth / 2;
+	_viewportPositionY = kPhotoHeight / 2;
+
+	updateViewport();
+
+	_screenHalfWidth  = kScreen.width() / 2;
+	_screenHalfHeight = kScreen.height() / 2;
+}
+
+void ESPER::resetSelectionRect() {
+	_selectionRect = kScreen;
+	_selectionCrosshairX = -1;
+	_selectionCrosshairY = -1;
+}
+
+void ESPER::resetSelectionBlinking() {
+	_selectionBlinkingCounter = 0;
+	_selectionBlinkingStyle = 0;
+	_timeSelectionBlinkingNext = 0;
+}
+
+void ESPER::resetPhotoZooming() {
+	_zoomStep = 0;
+	_timeZoomNext = 0;
+}
+
+void ESPER::resetPhotoOpening() {
+	_photoOpeningWidth = kScreen.left + 1;
+	_photoOpeningHeight = kScreen.top + 1;
+	_timePhotoOpeningNext = 0;
+}
+
+void ESPER::updateViewport() {
+	float halfWidth = (1.0f / 2.0f) * ((float)kPhotoWidth * (_zoomHorizontal / _zoom));
+	_viewport.left  = _viewportPositionX - halfWidth;
+	_viewport.right = _viewportPositionX + halfWidth;
+	if (_viewport.left < 0) {
+		_viewport.right -= _viewport.left;
+		_viewport.left = 0;
+	}
+	if (_viewport.right >= kPhotoWidth) {
+		_viewport.left -= _viewport.right - (kPhotoWidth - 1);
+		if (_viewport.left < 0) {
+			_viewport.left = 0;
+		}
+		_viewport.right = kPhotoWidth - 1;
+	}
+
+	float halfHeight = 1.0f / 2.0f * ((float)kPhotoHeight * (_zoomVertical / _zoom));
+	_viewport.top    = _viewportPositionY - halfHeight;
+	_viewport.bottom = _viewportPositionY + halfHeight;
+	if (_viewport.top < 0) {
+		_viewport.bottom -= _viewport.top;
+		_viewport.top = 0;
+	}
+	if (_viewport.bottom >= kPhotoHeight) {
+		_viewport.top -= _viewport.bottom - (kPhotoHeight - 1);
+		if (_viewport.top < 0) {
+			_viewport.top = 0;
+		}
+		_viewport.bottom = kPhotoHeight - 1;
+	}
+
+	_viewportWidth  = _viewport.right  + 1 - _viewport.left;
+	_viewportHeight = _viewport.bottom + 1 - _viewport.top;
+
+	int centerX = (_viewport.left + _viewport.right) / 2;
+	int centerY = (_viewport.top + _viewport.bottom) / 2;
+
+	float v50 = _zoom / _zoomHorizontal * 1.0f;
+	if ((_viewportPositionX > centerX + v50) || (_viewportPositionX < centerX - v50)) {
+		_viewportPositionX = centerX;
+	}
+
+	float v51 = _zoom / _zoomVertical * 1.0f;
+	if ((_viewportPositionY > centerY + v51) || (_viewportPositionY < centerY - v51)) {
+		_viewportPositionY = centerY;
+	}
+}
+
+void ESPER::activate(bool withOpening) {
+	_vm->_mouse->disable();
+
+	_buttons->resetImages();
+
+	if (withOpening) {
+		setStateMain(kEsperMainStateOpening);
+		playSound(413, 25);
+		wait(1000);
+		playSound(414, 25);
+		wait(2000);
+	} else {
+		_buttons->deactivate();
+		setStateMain(kEsperMainStateClear);
+	}
+
+	_buttons->activate(nullptr, nullptr, mouseDownCallback, mouseUpCallback, this);
+	_buttons->defineImage(kPhotoCount + 3, Common::Rect(42, 403, 76, 437), nullptr, nullptr, _shapeButton, nullptr);
+
+	playSound(415, 25);
+	wait(1000);
+
+	setStateMain(kEsperMainStateList);
+	resetPhotos();
+	_script->initialize();
+
+	_vm->_mouse->enable();
+}
+
+void ESPER::setStateMain(EsperMainStates state) {
+	if (_isOpen) {
+		_stateMain = state;
+		debug("ESPER main state: %d", _stateMain);
+
+	}
+}
+
+void ESPER::setStatePhoto(EsperPhotoStates state) {
+	_statePhoto = state;
+	debug("ESPER photo state: %d", _statePhoto);
+}
+
+void ESPER::wait(int timeout) {
+	if (!_isWaiting) {
+		_isWaiting = true;
+		uint timeEnd = timeout + _vm->getTotalPlayTime();
+		while (_vm->getTotalPlayTime() < timeEnd) {
+			_vm->gameTick();
+		}
+		_isWaiting = false;
+	}
+}
+
+void ESPER::playSound(int soundId, int volume) {
+	if (_soundId1 == -1) {
+		_soundId1 = soundId;
+		_volume1 = volume;
+	} else if (_soundId2 == -1) {
+		_soundId2 = soundId;
+		_volume2 = volume;
+	} else if (_soundId3 == -1) {
+		_soundId3 = soundId;
+		_volume3 = volume;
+	}
+}
+
+void ESPER::draw(Graphics::Surface &surface) {
+	if (!_isOpen) {
+		return;
+	}
+	_vqaMainPlayer->update(false);
+	switch (_stateMain) {
+		case kEsperMainStateOpening:
+		case kEsperMainStateList:
+			return;
+		case kEsperMainStatePhotoOpening:
+			drawPhotoOpening(surface);
+			break;
+		case kEsperMainStateClear:
+			surface.fillRect(kScreen, 0x0000);
+			break;
+		case kEsperMainStatePhoto:
+			if (_isScrolling) {
+				tickScroll();
+			}
+			switch (_statePhoto) {
+				case kEsperPhotoStateShow:
+					drawPhotoWithGrid(surface);
+					if (_isDrawingSelection) {
+						drawSelection(surface, true, 1);
+					}
+#if BLADERUNNER_DEBUG_RENDERING
+					for (int i = 0; i < kRegionCount; ++i) {
+						if (_regions[i].isPresent) {
+							surface.frameRect(
+								Common::Rect(
+									viewportXToScreenX(_regions[i].rectInner.left),
+									viewportYToScreenY(_regions[i].rectInner.top),
+									viewportXToScreenX(_regions[i].rectInner.right),
+									viewportYToScreenY(_regions[i].rectInner.bottom)
+								),
+								0x7FE0
+							);
+							surface.frameRect(
+								Common::Rect(
+									viewportXToScreenX(_regions[i].rectOuter.left),
+									viewportYToScreenY(_regions[i].rectOuter.top),
+									viewportXToScreenX(_regions[i].rectOuter.right),
+									viewportYToScreenY(_regions[i].rectOuter.bottom)
+								),
+								0x7FE0
+							);
+						}
+					}
+#endif
+					break;
+				case kEsperPhotoStateScrolling:
+					scrollUpdate();
+					drawPhotoWithGrid(surface);
+					break;
+				case kEsperPhotoStateSelectionZooming:
+					drawPhotoWithGrid(surface);
+					if (!drawSelectionZooming(surface)) {
+						setStatePhoto(kEsperPhotoStateSelectionBlinking);
+						playSound(418, 25);
+					}
+					break;
+				case kEsperPhotoStateSelectionBlinking:
+					drawPhotoWithGrid(surface);
+					if (!drawSelectionBlinking(surface)) {
+						setStatePhoto(kEsperPhotoStatePhotoZooming);
+					}
+					break;
+				case kEsperPhotoStatePhotoZooming:
+					drawPhotoZooming(surface);
+					break;
+				case kEsperPhotoStatePhotoSharpening:
+					drawPhotoSharpening(surface);
+					break;
+				case kEsperPhotoStatePhotoZoomOut:
+					drawPhotoZoomOut(surface);
+					break;
+				case kEsperPhotoStateVideoZooming:
+					drawVideoZooming(surface);
+					break;
+				case kEsperPhotoStateVideoShow:
+					drawVideoFrame(surface);
+					drawGrid(surface);
+					break;
+				case kEsperPhotoStateVideoZoomOut:
+					drawVideoZoomOut(surface);
+					break;
+				default:
+					break;
+			}
+			drawTextCoords(surface);
+
+			break;
+	}
+}
+
+void ESPER::drawPhotoOpening(Graphics::Surface &surface) {
+	bool needMoreZooming = true;
+	int timeNow = _vm->getTotalPlayTime();
+	if (timeNow >= _timePhotoOpeningNext) {
+		_photoOpeningWidth = MIN(_photoOpeningWidth + 8, kScreen.right - 1);
+		_photoOpeningHeight = MIN(_photoOpeningHeight + 7, kScreen.bottom - 1);
+
+		if (_photoOpeningWidth == kScreen.right - 1 && _photoOpeningHeight == kScreen.bottom - 1) {
+			needMoreZooming = false;
+		}
+
+		_timePhotoOpeningNext = timeNow + 20;
+	}
+	copyImageScale(_photoData, _viewport, &surface, Common::Rect(kScreen.left, kScreen.top, _photoOpeningWidth, _photoOpeningHeight));
+
+	surface.hLine(kScreen.left,         _photoOpeningHeight,     kScreen.right - 1,  0x03E0);
+	surface.vLine(_photoOpeningWidth,     kScreen.top,           kScreen.bottom - 1, 0x03E0);
+	surface.hLine(kScreen.left,         _photoOpeningHeight - 1, kScreen.right - 1,  0x0240);
+	surface.vLine(_photoOpeningWidth - 1, kScreen.top,           kScreen.bottom - 1, 0x0240);
+
+	drawGrid(surface);
+
+	if (!needMoreZooming) {
+		setStateMain(kEsperMainStatePhoto);
+		setStatePhoto(kEsperPhotoStateShow);
+		_vm->_mouse->enable();
+	}
+}
+
+bool ESPER::drawSelectionZooming(Graphics::Surface &surface) {
+	bool zooming = false;
+	bool needMoreZooming = true;
+	int timeNow = _vm->getTotalPlayTime();
+	if (timeNow > _timeSelectionZoomNext) {
+		zooming = true;
+		_selectionRect.left   += _selectionRectDelta.left;
+		_selectionRect.top    += _selectionRectDelta.top;
+		_selectionRect.right  += _selectionRectDelta.right;
+		_selectionRect.bottom += _selectionRectDelta.bottom;
+		++_selectionZoomStep;
+		_timeSelectionZoomNext = timeNow + 150;
+		if (_selectionZoomStep > kSelectionZoomSteps) {
+			needMoreZooming = false;
+			_selectionRect.left   = _selectionRectTarget.left;
+			_selectionRect.top    = _selectionRectTarget.top;
+			_selectionRect.right  = _selectionRectTarget.right;
+			_selectionRect.bottom = _selectionRectTarget.bottom;
+		}
+	}
+	drawSelection(surface, false, 1);
+	if (!needMoreZooming) {
+		_statePhoto = kEsperPhotoStatePhotoZooming;
+		resetPhotoZooming();
+		zooming = false;
+	}
+	if (zooming) {
+		playSound(416, 20);
+	}
+	return needMoreZooming;
+}
+
+bool ESPER::drawSelectionBlinking(Graphics::Surface &surface) {
+	bool needMoreBlinking = true;
+	int timeNow = _vm->getTotalPlayTime();
+	if (timeNow > _timeSelectionBlinkingNext) {
+		_timeSelectionBlinkingNext = timeNow + 100;
+		_selectionBlinkingStyle ^= 1;
+		++_selectionBlinkingCounter;
+		if (_selectionBlinkingCounter > 10) {
+			needMoreBlinking = false;
+			_selectionBlinkingStyle = 0;
+		}
+	}
+	drawSelection(surface, false, _selectionBlinkingStyle);
+	if (!needMoreBlinking) {
+		resetSelectionBlinking();
+	}
+	return needMoreBlinking;
+}
+
+void ESPER::drawPhotoZooming(Graphics::Surface &surface) {
+	int timeNow = _vm->getTotalPlayTime();
+	if ((timeNow > _timeZoomNext) && (_zoomStep < _zoomSteps)) {
+		_flash = true;
+
+		_viewportPositionXCurrent += _viewportPositionXDelta;
+		_viewportPositionYCurrent += _viewportPositionYDelta;
+		_viewportPositionX = _viewportPositionXCurrent;
+		_viewportPositionY = _viewportPositionYCurrent;
+
+		_zoom += _zoomDelta;
+		if (_zoomDelta > 0.0f) {
+			if (_zoom > _zoomTarget) {
+				_zoom = _zoomTarget;
+				_zoomStep = _zoomSteps;
+			} else {
+				_blur += _zoomDelta * 2.0f;
+			}
+		} else if (_zoomDelta < 0.0f) {
+			if (_zoom < _zoomTarget) {
+				_zoom = _zoomTarget;
+				_zoomStep = _zoomSteps;
+			}
+		}
+		++_zoomStep;
+		if (_zoomStep >= _zoomSteps) {
+			_zoom = _zoomTarget;
+			_viewportPositionX = _viewportPositionXTarget;
+			_viewportPositionY = _viewportPositionYTarget;
+		}
+		updateViewport();
+		_timeZoomNext = timeNow + 300;
+	}
+
+	if (_zoomDelta >= 0.0f) {
+		drawPhoto(surface);
+	} else {
+		drawPhotoWithGrid(surface);
+	}
+	drawGrid(surface);
+
+	if ((timeNow > _timeZoomNext) && (_zoomStep >= _zoomSteps)) {
+		if (_regionSelectedAck) {
+			if (_regions[_regionSelected].name[0]) {
+				if (_zoomDelta < 0.0f) {
+					_blur = 1.0f;
+					_zoomDelta = (_zoom * 1.5f - _zoom) / (float)_zoomSteps; // 0.5f * _zoom ???
+				}
+				setStatePhoto(kEsperPhotoStateVideoZooming);
+				_timeZoomNext += 300;
+			} else {
+				_regionSelectedAck = false;
+				_selectionRect.left   = viewportXToScreenX(_regions[_regionSelected].rectInner.left);
+				_selectionRect.right  = viewportXToScreenX(_regions[_regionSelected].rectInner.right);
+				_selectionRect.top    = viewportYToScreenY(_regions[_regionSelected].rectInner.top);
+				_selectionRect.bottom = viewportYToScreenY(_regions[_regionSelected].rectInner.bottom);
+				prepareZoom();
+				resetPhotoZooming();
+				updateSelection();
+				setStatePhoto(kEsperPhotoStatePhotoZooming);
+			}
+		} else {
+			setStatePhoto(kEsperPhotoStatePhotoSharpening);
+		}
+		resetPhotoOpening();
+	}
+}
+
+void ESPER::drawPhotoSharpening(Graphics::Surface &surface) {
+	int timeNow = _vm->getTotalPlayTime();
+	bool needMoreSharpening = true;
+	if (timeNow >= _timePhotoOpeningNext) {
+		_photoOpeningWidth = MIN(_photoOpeningWidth + 8, kScreen.right - 1);
+		_photoOpeningHeight = MIN(_photoOpeningHeight + 7, kScreen.bottom - 1);
+
+		if (_photoOpeningWidth == kScreen.right - 1 && _photoOpeningHeight == kScreen.bottom - 1) {
+			needMoreSharpening = false;
+		}
+
+		_timePhotoOpeningNext = timeNow + 50;
+	}
+
+	if (_regionSelectedAck && _regions[_regionSelected].name[0]) {
+		_vqaPhotoPlayer->update(true, false);
+		copyImageBlur(_viewportData, Common::Rect(0, 0, 299, 263), &surface, kScreen, _blur);
+		copyImageBlit(_viewportData, Common::Rect(0, 0, 0, 0), &surface, Common::Rect(kScreen.left, kScreen.top, _photoOpeningWidth, _photoOpeningHeight));
+	} else {
+		drawPhoto(surface);
+		copyImageScale(_photoData, _viewport, _viewportData, Common::Rect(0, 0, kScreen.width(), kScreen.height()));
+		copyImageBlit(_viewportData, Common::Rect(0, 0, 0, 0), &surface, Common::Rect(kScreen.left, kScreen.top, _photoOpeningWidth, _photoOpeningHeight));
+
+	}
+	drawGrid(surface);
+	surface.hLine(kScreen.left,           _photoOpeningHeight,     kScreen.right  - 1, 0x03E0);
+	surface.vLine(_photoOpeningWidth,     kScreen.top,             kScreen.bottom - 1, 0x03E0);
+	surface.hLine(kScreen.left,           _photoOpeningHeight - 1, kScreen.right  - 1, 0x0240);
+	surface.vLine(_photoOpeningWidth - 1, kScreen.top,             kScreen.bottom - 1, 0x0240);
+	if (!needMoreSharpening) {
+		if (_regionSelectedAck && _regions[_regionSelected].name[0]){
+			setStatePhoto(kEsperPhotoStateVideoShow);
+		} else {
+			setStatePhoto(kEsperPhotoStateShow);
+		}
+		resetPhotoZooming();
+		resetPhotoOpening();
+		_vm->_mouse->enable();
+	}
+}
+
+void ESPER::drawPhotoZoomOut(Graphics::Surface &surface) {
+	int timeNow = _vm->getTotalPlayTime();
+	if (timeNow >= _timeZoomOutNext) {
+		_timeZoomOutNext = timeNow + 300;
+
+		if (_zoom > _zoomMin) {
+			_zoom /= 1.3f;
+			_flash = true;
+			if (_zoomHorizontal <= _zoomVertical) {
+				if (_zoom < _zoomVertical) {
+					_zoom = _zoomVertical;
+				}
+			} else {
+				if (_zoom < _zoomHorizontal) {
+					_zoom = _zoomHorizontal;
+				}
+			}
+			updateViewport();
+		} else {
+			_statePhoto = kEsperPhotoStateShow;
+		}
+	}
+	drawPhotoWithGrid(surface);
+}
+
+void ESPER::drawVideoZooming(Graphics::Surface &surface) {
+	if (_vqaPhotoPlayer == nullptr) {
+		_vqaPhotoPlayer = new VQAPlayer(_vm, _viewportData);
+		if (!_vqaPhotoPlayer->open(Common::String(_regions[_regionSelected].name) + ".VQA")) {
+			setStatePhoto(kEsperPhotoStateShow);
+			_vm->_mouse->enable();
+
+			delete _vqaPhotoPlayer;
+			_vqaPhotoPlayer = nullptr;
+
+			return;
+		}
+
+		_timeZoomNext = 0;
+	}
+
+	bool flash = false;
+	bool advanceFrame = false;
+	int timeNow = _vm->getTotalPlayTime();
+	if (timeNow > _timeZoomNext) {
+		_timeZoomNext = timeNow + 300;
+		playSound(419, 25);
+		flash = true;
+		advanceFrame = true;
+		_blur += _zoomDelta * 5.0f;
+	}
+
+	int frame = _vqaPhotoPlayer->update(true, advanceFrame);
+	if (frame == _vqaPhotoPlayer->getFrameCount() - 1) {
+		_vqaLastFrame = frame;
+		setStatePhoto(kEsperPhotoStatePhotoSharpening);
+	}
+
+	if (flash) {
+		flashViewport();
+	}
+	copyImageBlur(_viewportData, Common::Rect(0, 0, 299, 263), &surface, kScreen, _blur);
+	drawGrid(surface);
+}
+
+void ESPER::drawVideoZoomOut(Graphics::Surface &surface) {
+	bool flash = false;
+	bool advanceFrame = false;
+	int timeNow = _vm->getTotalPlayTime();
+	if (timeNow > _timeZoomNext && _vqaLastFrame > 0) {
+		_timeZoomNext = timeNow + 300;
+		playSound(419, 25);
+		//TODO: implement frame loading after seek, then advanceFrame can be removed
+		_vqaPhotoPlayer->seekToFrame(_vqaLastFrame);
+		int nextFrame = _vqaPhotoPlayer->getFrameCount() / 4;
+		if (nextFrame <= 0) {
+			nextFrame = 1;
+		} else if (nextFrame > 4) {
+			nextFrame = 4;
+		}
+		flash = true;
+		advanceFrame = true;
+		_vqaLastFrame -= nextFrame;
+	}
+
+	_vqaPhotoPlayer->update(true, advanceFrame);
+	if (flash) {
+		flashViewport();
+	}
+	copyImageBlit(_viewportData, Common::Rect(0, 0, 0, 0), &surface, kScreen);
+	drawGrid(surface);
+	if (timeNow > _timeZoomNext && _vqaLastFrame <= 0) {
+		_vqaPhotoPlayer->close();
+		delete _vqaPhotoPlayer;
+		_vqaPhotoPlayer = nullptr;
+
+		//TODO: there is code to stop zooming, but it is not working properly in original game
+		// if (_isMouseDown) {
+		// 	zoomOutStart();
+		// } else {
+		// 	zoomOutStop();
+		// }
+		zoomOutStart();
+	}
+}
+
+void ESPER::drawPhoto(Graphics::Surface &surface) {
+	copyImageBlur(_photoData, _viewport, &surface, kScreen, _blur);
+}
+
+void ESPER::drawGrid(Graphics::Surface &surface) {
+	for (int i = 0; i < 7; ++i) {
+		surface.drawLine(kScreen.left + i * 50, kScreen.top, kScreen.left + i * 50, kScreen.bottom - 1, 0x109C);
+	}
+
+	for (int i = 0; i < 7; ++i) {
+		surface.drawLine(kScreen.left, kScreen.top + i * 44, kScreen.right - 1, kScreen.top + i * 44, 0x109C);
+	}
+}
+
+void ESPER::drawPhotoWithGrid(Graphics::Surface &surface) {
+	copyImageScale(_photoData, _viewport, &surface, kScreen);
+	drawGrid(surface);
+}
+
+void ESPER::drawSelection(Graphics::Surface &surface, bool crosshair, int style) {
+	int left   = CLIP(_selectionRect.left,   kScreen.left, (int16)(kScreen.right  - 1));
+	int top    = CLIP(_selectionRect.top,    kScreen.top,  (int16)(kScreen.bottom - 1));
+	int right  = CLIP(_selectionRect.right,  kScreen.left, (int16)(kScreen.right  - 1));
+	int bottom = CLIP(_selectionRect.bottom, kScreen.top,  (int16)(kScreen.bottom - 1));
+
+	int color = 0x0240;
+	if (style) {
+		color = 0x03E0;
+	}
+
+	// selection rectangle
+	Common::Rect selectedRect(MIN(left, right), MIN(top, bottom), MAX(left, right) + 1, MAX(top, bottom) + 1);
+	Common::Rect selectedRect2 = selectedRect;
+	selectedRect2.grow(-1);
+	surface.frameRect(selectedRect, color);
+	surface.frameRect(selectedRect2, color);
+
+	if (crosshair) {
+		if (_selectionCrosshairX == -1) {
+			if (_selectionRect.left < (kScreen.left + kScreen.right) / 2) {
+				_selectionCrosshairX = kScreen.left;
+			} else {
+				_selectionCrosshairX = kScreen.right - 1;
+			}
+		}
+		if (_selectionCrosshairY == -1) {
+			if (_selectionRect.top < (kScreen.top + kScreen.bottom) / 2) {
+				_selectionCrosshairY = kScreen.top;
+			} else {
+				_selectionCrosshairY = kScreen.bottom - 1;
+			}
+		}
+
+		// ghosting
+		if (_selectionCrosshairX != right) {
+			surface.vLine(_selectionCrosshairX, kScreen.top, kScreen.bottom - 1, 0x0240);
+			if (abs(_selectionCrosshairX - right) <= 1) {
+				_selectionCrosshairX = right;
+			} else {
+				_selectionCrosshairX = (_selectionCrosshairX + right) / 2;
+			}
+		}
+		if (_selectionCrosshairY != bottom) {
+			surface.hLine(kScreen.left, _selectionCrosshairY, kScreen.right - 1, 0x0240);
+			if (abs(_selectionCrosshairY - bottom) <= 1) {
+				_selectionCrosshairY = bottom;
+			} else {
+				_selectionCrosshairY = (_selectionCrosshairY + bottom) / 2;
+			}
+		}
+
+		surface.vLine(right, kScreen.top, kScreen.bottom - 1, 0x03E0);
+		surface.hLine(kScreen.left, bottom, kScreen.right - 1, 0x03E0);
+	}
+}
+
+void ESPER::drawVideoFrame(Graphics::Surface &surface) {
+	_vqaPhotoPlayer->update(true, false);
+	copyImageBlit(_viewportData, Common::Rect(0, 0, 0, 0), &surface, kScreen);
+}
+
+void ESPER::drawTextCoords(Graphics::Surface &surface) {
+	_vm->_mainFont->drawColor(Common::String::format("ZM %04.0f", _zoom / _zoomMin * 2.0f  ), surface, 155, 364, 0x001F);
+	_vm->_mainFont->drawColor(Common::String::format("NS %04d",   12 * _viewport.top  + 98 ), surface, 260, 364, 0x001F);
+	_vm->_mainFont->drawColor(Common::String::format("EW %04d",   12 * _viewport.left + 167), surface, 364, 364, 0x001F);
+}
+
+void ESPER::flashViewport() {
+	uint16 *ptr = (uint16 *)_viewportData->getPixels();
+	for (int i = 0; i < _viewportData->w * _viewportData->h; ++i) {
+		int8 r = (*ptr >> 10) & 0x1F;
+		int8 g = (*ptr >>  5) & 0x1F;
+		int8 b = (*ptr      ) & 0x1F;
+		b = MIN(b * 2, 31);
+		*ptr = r << 10 | g << 5 | b;
+
+		++ptr;
+	}
+}
+
+void ESPER::copyImageScale(Graphics::Surface *src, Common::Rect srcRect, Graphics::Surface *dst, Common::Rect dstRect) {
+	if (_flash) {
+		playSound(419, 25);
+	}
+
+	int srcDstWidthRatio  = srcRect.width()  / dstRect.width();
+	int srcDstWidthRest   = srcRect.width()  % dstRect.width();
+	int srcDstHeightRatio = srcRect.height() / dstRect.height();
+	int srcDstHeightRest  = srcRect.height() % dstRect.height();
+
+	if (srcRect.width() > dstRect.width() && srcRect.height() > dstRect.height()) {
+		// reduce
+		int srcY = srcRect.top;
+		int srcYCounter = 0;
+		for (int dstY = dstRect.top; dstY < dstRect.bottom; ++dstY) {
+			int srcX = srcRect.left;
+			int srcXCounter = 0;
+			for (int dstX = dstRect.left; dstX < dstRect.right; ++dstX) {
+				uint16 *srcPtr = (uint16 *)src->getBasePtr(srcX, srcY);
+				uint16 *dstPtr = (uint16 *)dst->getBasePtr(dstX, dstY);
+
+				if (_flash) {
+					int8 r = (*srcPtr >> 10) & 0x1F;
+					int8 g = (*srcPtr >>  5) & 0x1F;
+					int8 b = (*srcPtr      ) & 0x1F;
+					// add blue-ish tint
+					b = MIN(b * 2, 31);
+					*dstPtr = r << 10 | g << 5 | b;
+				} else {
+					*dstPtr = *srcPtr;
+				}
+
+				srcX += srcDstWidthRatio;
+				srcXCounter += srcDstWidthRest;
+				if (srcXCounter >= dstRect.width()) {
+					srcXCounter -= dstRect.width();
+					++srcX;
+				}
+			}
+
+			srcY += srcDstHeightRatio;
+			srcYCounter += srcDstHeightRest;
+			if (srcYCounter >= dstRect.height()) {
+				srcYCounter -= dstRect.height();
+				++srcY;
+			}
+		}
+	} else {
+		// enlarge
+		int srcY = srcRect.top;
+		int srcYCounter = 0;
+		for (int dstY = dstRect.top; dstY < dstRect.bottom; ++dstY) {
+			int srcX = srcRect.left;
+			int srcXCounter = 0;
+			for (int dstX = dstRect.left; dstX < dstRect.right; ++dstX) {
+				srcXCounter += srcRect.width();
+				if (srcXCounter >= dstRect.width()) {
+					srcXCounter -= dstRect.width();
+					++srcX;
+				}
+				uint16 *srcPtr = (uint16 *)src->getBasePtr(srcX, srcY);
+				uint16 *dstPtr = (uint16 *)dst->getBasePtr(dstX, dstY);
+
+				if (_flash) {
+					int8 r = (*srcPtr >> 10) & 0x1F;
+					int8 g = (*srcPtr >>  5) & 0x1F;
+					int8 b = (*srcPtr      ) & 0x1F;
+					// add blue-ish tint
+					b = MIN(b * 2, 31);
+					*dstPtr = r << 10 | g << 5 | b;
+				} else {
+					*dstPtr = *srcPtr;
+				}
+			}
+
+			srcYCounter += srcRect.height();
+			if (srcYCounter >= dstRect.height()) {
+				srcYCounter -= dstRect.height();
+				++srcY;
+			}
+		}
+	}
+	_flash = false;
+}
+
+void ESPER::copyImageBlur(Graphics::Surface *src, Common::Rect srcRect, Graphics::Surface *dst, Common::Rect dstRect, float blur) {
+	if (_flash) {
+		playSound(419, 25);
+	}
+
+	int srcDstWidthRatio  = srcRect.width()  / dstRect.width();
+	int srcDstWidthRest   = srcRect.width()  % dstRect.width();
+	int srcDstHeightRatio = srcRect.height() / dstRect.height();
+	int srcDstHeightRest  = srcRect.height() % dstRect.height();
+
+	int skipStep = (blur - (int)blur) * 1000.0f;
+	if (srcRect.width() > dstRect.width() && srcRect.height() > dstRect.height()) {
+		// reduce
+		int srcY = srcRect.top;
+		int dstY = dstRect.top;
+		int srcYCounter = 0;
+		int skipYMaxCounter = 0;
+		while (dstY < dstRect.bottom) {
+			skipYMaxCounter += skipStep;
+			int skipYMax = blur;
+			if (skipYMaxCounter >= 1000) {
+				skipYMaxCounter -= 1000;
+				++skipYMax;
+			}
+			int skipY = 0;
+			while (dstY < dstRect.bottom && skipY < skipYMax) {
+				int srcX = srcRect.left;
+				int dstX = dstRect.left;
+				int srcXCounter = 0;
+				int skipXMaxCounter = 0;
+				while (dstX < dstRect.right) {
+					skipXMaxCounter += skipStep;
+					int skipXMax = blur;
+					if (skipXMaxCounter >= 1000) {
+						skipXMaxCounter -= 1000;
+						++skipXMax;
+					}
+					int skipX = 0;
+					while (dstX < dstRect.right && skipX < skipXMax) {
+						uint16 *srcPtr = (uint16 *)src->getBasePtr(srcX, srcY);
+						uint16 *dstPtr = (uint16 *)dst->getBasePtr(dstX, dstY);
+
+						if (_flash) {
+							int8 r = (*srcPtr >> 10) & 0x1F;
+							int8 g = (*srcPtr >>  5) & 0x1F;
+							int8 b = (*srcPtr      ) & 0x1F;
+							// add blue-ish tint
+							b = MIN(b * 2, 31);
+							*dstPtr = r << 10 | g << 5 | b;
+						} else {
+							*dstPtr = *srcPtr;
+						}
+
+						++dstX;
+						++skipX;
+					}
+					srcXCounter += srcDstWidthRest;
+					srcX += srcDstWidthRatio * skipX;
+					if (srcXCounter >= dstRect.width()) {
+						srcXCounter -= dstRect.width();
+						srcX += skipX;
+					}
+				}
+
+				++dstY;
+				++skipY;
+			}
+
+			srcYCounter += srcDstHeightRest;
+			srcY += srcDstHeightRatio * skipY;
+			if (srcYCounter >= dstRect.height()) {
+				srcYCounter -= dstRect.height();
+				srcY += skipY;
+			}
+		}
+	} else {
+		// enlarge
+		int srcY = srcRect.top;
+		int dstY = dstRect.top;
+		int srcYCounter = srcRect.height(); // TODO: look at this again because in original source this is 0, but then first line is doubled
+		int skipYMaxCounter = 0;
+		while (dstY < dstRect.bottom) {
+			skipYMaxCounter += skipStep;
+			int skipYMax = blur;
+			if (skipYMaxCounter >= 1000) {
+				skipYMaxCounter -= 1000;
+				++skipYMax;
+			}
+			int skipY = 0;
+			while (dstY < dstRect.bottom && skipY < skipYMax) {
+				int srcX = srcRect.left;
+				int dstX = dstRect.left;
+				int srcXCounter = 0;
+				int skipXMaxCounter = 0;
+				while (dstX < dstRect.right) {
+					skipXMaxCounter += skipStep;
+					int skipXMax = blur;
+					if (skipXMaxCounter >= 1000) {
+						skipXMaxCounter -= 1000;
+						++skipXMax;
+					}
+					int skipX = 0;
+					while (dstX < dstRect.right && skipX < skipXMax) {
+						srcXCounter += srcRect.width();
+						if (srcXCounter >= dstRect.width()) {
+							srcXCounter -= dstRect.width();
+							srcX += 1; // bug in original game? Is using 1 instead of skipX as for Y
+						}
+
+						uint16 *srcPtr = (uint16 *)src->getBasePtr(srcX, srcY);
+						uint16 *dstPtr = (uint16 *)dst->getBasePtr(dstX, dstY);
+
+						if (_flash) {
+							int8 r = (*srcPtr >> 10) & 0x1F;
+							int8 g = (*srcPtr >>  5) & 0x1F;
+							int8 b = (*srcPtr      ) & 0x1F;
+							// add blue-ish tint
+							b = MIN(b * 2, 31);
+							*dstPtr = r << 10 | g << 5 | b;
+						} else {
+							*dstPtr = *srcPtr;
+						}
+
+						++dstX;
+						++skipX;
+					}
+				}
+
+				++dstY;
+				++skipY;
+			}
+
+			srcYCounter += srcRect.height();
+			if (srcYCounter >= dstRect.height()) {
+				srcYCounter -= dstRect.height();
+				srcY += skipY;
+			}
+		}
+	}
+	_flash = false;
+}
+
+void ESPER::copyImageBlit(Graphics::Surface *src, Common::Rect srcRect, Graphics::Surface *dst, Common::Rect dstRect) {
+	for (int y = 0; y < dstRect.height(); ++y) {
+		for (int x = 0; x < dstRect.width(); ++x) {
+			uint16 *srcPtr = (uint16 *)src->getBasePtr(srcRect.left + x, srcRect.top + y);
+			uint16 *dstPtr = (uint16 *)dst->getBasePtr(dstRect.left + x, dstRect.top + y);
+			*dstPtr = *srcPtr;
+		}
+	}
+}
+
+void ESPER::tickSound() {
+	if (_soundId1 != -1) {
+		_vm->_audioPlayer->playAud(_vm->_gameInfo->getSfxTrack(_soundId1), _volume1, 0, 0, 50, 0);
+		_soundId1 = -1;
+	}
+	if (_soundId2 != -1) {
+		_vm->_audioPlayer->playAud(_vm->_gameInfo->getSfxTrack(_soundId2), _volume2, 0, 0, 50, 0);
+		_soundId2 = -1;
+	}
+	if (_soundId3 != -1) {
+		_vm->_audioPlayer->playAud(_vm->_gameInfo->getSfxTrack(_soundId3), _volume3, 0, 0, 50, 0);
+		_soundId3 = -1;
+	}
+}
+
+void ESPER::tickMouse(Graphics::Surface &surface) {
+	if (_vm->_mouse->isDisabled()) {
+		return;
+	}
+
+	int cursor = -1;
+
+	Common::Point p = _vm->getMousePos();
+
+	_mouseOverScroll = 4;
+	if (_stateMain == kEsperMainStatePhoto) {
+		if (kScreen.contains(p)) {
+			if (_statePhoto == kEsperPhotoStateShow) {
+				if ( _zoom != 2.0f) {
+					if (_isMouseDown) {
+						if (_isDrawingSelection) {
+							_selectionRect.right = p.x;
+							_selectionRect.bottom = p.y;
+						} else {
+							_selectionRect.left = p.x;
+							_selectionRect.top = p.y;
+							_selectionRect.right = p.x + 1;
+							_selectionRect.bottom = p.y + 1;
+							_isDrawingSelection = true;
+						}
+					} else {
+						if (_isDrawingSelection) {
+							_selectionRect.right = p.x;
+							_selectionRect.bottom = p.y;
+							if (_selectionRect.right < _selectionRect.left) {
+								SWAP(_selectionRect.left, _selectionRect.right);
+							}
+							if (_selectionRect.bottom < _selectionRect.top) {
+								SWAP(_selectionRect.bottom, _selectionRect.top);
+							}
+
+							if (_selectionRect.right >= _selectionRect.left + 3) {
+								updateSelection();
+								_vm->_mouse->disable();
+								zoomingStart();
+							} else {
+								resetSelectionRect();
+							}
+						}
+						_isDrawingSelection = false;
+					}
+				}
+			}
+			surface.vLine(p.x,     p.y - 8, p.y - 1, 0x03E0);
+			surface.vLine(p.x,     p.y + 8, p.y + 1, 0x03E0);
+			surface.hLine(p.x - 8, p.y,     p.x - 1, 0x03E0);
+			surface.hLine(p.x + 8, p.y,     p.x + 1, 0x03E0);
+			_mouseOverScroll = -1;
+		} else if (p.x >= 85 && p.y >= 73 && p.x <= 484 && p.y <= 436) {
+			if (!_isDrawingSelection && _statePhoto != kEsperPhotoStateVideoShow && _zoom != 2.0f) {
+				_mouseOverScroll = (angle_1024((kScreen.left + kScreen.right) / 2, (kScreen.top + kScreen.bottom) / 2, p.x, p.y) + 128) / 256;
+				if (_mouseOverScroll >= 4) {
+					_mouseOverScroll = 0;
+				}
+				if (_mouseOverScroll == 0 && this->_viewport.top == 0) {
+					_mouseOverScroll = 4;
+				} else if (_mouseOverScroll == 1 && this->_viewport.right == kPhotoWidth - 1) {
+					_mouseOverScroll = 4;
+				} else if (_mouseOverScroll == 2 && this->_viewport.bottom ==  kPhotoHeight - 1) {
+					_mouseOverScroll = 4;
+				} else if (_mouseOverScroll == 3 && this->_viewport.left == 0) {
+					_mouseOverScroll = 4;
+				}
+				if (_mouseOverScroll != 4) {
+					cursor = _mouseOverScroll + 2;
+				}
+			}
+		}
+	}
+
+	if (_mouseOverScroll == 4) {
+		cursor = _buttons->hasHoveredImage() ? 1 : 0;
+	}
+	if (cursor != -1) {
+		_vm->_mouse->setCursor(cursor);
+		_vm->_mouse->draw(surface, p.x, p.y);
+	}
+}
+
+void ESPER::tickScroll() {
+	int timeNow = _vm->getTotalPlayTime();
+	if (timeNow <= _timeScrollNext) {
+		return;
+	}
+	_timeScrollNext = timeNow + 300;
+
+	if (_scrollingDirection == 0) {
+		scrollUp();
+	} else if (_scrollingDirection == 1) {
+		scrollRight();
+	} else if (_scrollingDirection == 2) {
+		scrollDown();
+	} else if (_scrollingDirection == 3) {
+		scrollLeft();
+	}
+}
+
+int ESPER::findEmptyPhoto() {
+	for (int i = 0; i < kPhotoCount; ++i) {
+		if (!_photos[i].isPresent) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+void ESPER::selectPhoto(int photoId) {
+	_vm->_mouse->disable();
+	_photoIdSelected = _photos[photoId].photoId;
+	unloadPhotos();
+	_script->photoSelected(_photoIdSelected);
+
+	Common::ScopedPtr<Common::SeekableReadStream> s(_vm->getResourceStream(_photos[photoId].name));
+
+	if (!s) {
+		reset();
+	}
+
+	int photoSize = _photoData->w * _photoData->h * _photoData->format.bytesPerPixel;
+
+	s->skip(3); // not used, but there is compression type
+	uint width  = s->readUint32LE();
+	uint height = s->readUint32LE();
+	int photoCompressedSize = s->size() - s->pos();
+	uint8 *photoCompressed = (uint8 *)_photoData->getPixels() + photoSize - photoCompressedSize;
+	s->read(photoCompressed, photoCompressedSize);
+
+	decompress_lcw(photoCompressed, photoCompressedSize, (uint8 *)_photoData->getPixels(), photoSize);
+
+	// apply palette
+	for (uint j = 0; j < width * height; ++j) {
+		// _photoData[j] = Palette[_photoData[j]];
+	}
+
+	_shapeThumbnail = new Shape(_vm);
+	_shapeThumbnail->open("ESPTHUMB.SHP", _photos[photoId].shapeId);
+	_buttons->resetImages();
+	_buttons->defineImage(kPhotoCount + 2, Common::Rect(480, 350, 578, 413), _shapeThumbnail, _shapeThumbnail, _shapeThumbnail, nullptr);
+	_buttons->defineImage(kPhotoCount + 3, Common::Rect(42, 403, 76, 437), nullptr, nullptr, _shapeButton, nullptr);
+
+	resetPhotoOpening();
+	resetViewport();
+	setStateMain(kEsperMainStatePhotoOpening);
+	setStatePhoto(kEsperPhotoStateOpening);
+	playSound(422, 25);
+	playSound(423, 25);
+}
+
+void ESPER::unloadPhotos() {
+	for (int i = 0; i < kPhotoCount; ++i) {
+		if (_photos[i].isPresent) {
+			_buttons->resetImage(i);
+			delete _shapesPhotos[i];
+			_shapesPhotos[i] = nullptr;
+			_photos[i].isPresent = false;
+		}
+	}
+}
+
+int ESPER::findEmptyRegion() {
+	for (int i = 0; i < kRegionCount; ++i) {
+		if (!_regions[i].isPresent) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+int ESPER::findRegion(Common::Rect where) {
+	for (int i = 0; i < kRegionCount; ++i) {
+		if (_regions[i].isPresent && _regions[i].rectOuter.contains(where) && where.contains(_regions[i].rectInner)){
+			return i;
+		}
+	}
+	return -1;
+}
+
+void ESPER::zoomingStart() {
+	prepareZoom();
+	setStatePhoto(kEsperPhotoStateSelectionZooming);
+}
+
+void ESPER::zoomOutStart() {
+	if (_statePhoto == kEsperPhotoStateVideoShow) {
+		resetPhotoZooming();
+		setStatePhoto(kEsperPhotoStateVideoZoomOut);
+	} else {
+		zoomOutStop();
+		if (_zoomMin < _zoom) {
+			_isZoomingOut = true;
+			setStatePhoto(kEsperPhotoStatePhotoZoomOut);
+		}
+	}
+}
+
+void ESPER::zoomOutStop() {
+	_isZoomingOut = false;
+	_statePhoto = kEsperPhotoStateShow;
+}
+
+void ESPER::scrollingStart(int direction) {
+	scrollingStop();
+	if ((direction != 3 || _viewport.left > 0)
+	 && (direction != 0 || _viewport.top > 0)
+	 && (direction != 1 || _viewport.right != kPhotoWidth - 1)
+	 && (direction != 2 || _viewport.bottom != kPhotoHeight - 1)) {
+		_isScrolling = true;
+		_scrollingDirection = direction;
+	}
+}
+
+void ESPER::scrollingStop() {
+	_isScrolling = false;
+	_scrollingDirection = -1;
+}
+
+void ESPER::scrollUpdate() {
+	if ((_viewport.left == _viewportNext.left) && (_viewportNext.top == _viewport.top)) {
+		setStatePhoto(kEsperPhotoStateShow);
+		return;
+	}
+
+	if (_viewport.left != _viewportNext.left) {
+		_viewport.left     = _viewportNext.left;
+		_viewport.right    = _viewportNext.right;
+		_viewportPositionX = (_viewportNext.left + _viewportNext.right) / 2;
+	}
+
+	if (_viewport.top != _viewportNext.top) {
+		_viewport.top      = _viewportNext.top;
+		_viewport.bottom   = _viewportNext.bottom;
+		_viewportPositionY = (_viewportNext.top + _viewportNext.bottom) / 2;
+	}
+}
+
+void ESPER::scrollLeft() {this->_flash = 1;
+	_flash = true;
+	setStatePhoto(kEsperPhotoStateScrolling);
+
+	_viewportNext.left  = _viewport.left  - 40;
+	_viewportNext.right = _viewport.right - 40;
+	if (_viewportNext.left < 0) {
+		_viewportNext.right -= _viewportNext.left;
+		_viewportNext.left   = 0;
+		scrollingStop();
+	}
+	_viewportNext.top    = _viewport.top;
+	_viewportNext.bottom = _viewport.bottom;
+}
+
+void ESPER::scrollUp() {
+	_flash = true;
+	setStatePhoto(kEsperPhotoStateScrolling);
+
+	_viewportNext.top    = _viewport.top    - 40;
+	_viewportNext.bottom = _viewport.bottom - 40;
+	if (_viewportNext.top < 0) {
+		_viewportNext.bottom -= _viewportNext.top;
+		_viewportNext.top     = 0;
+		scrollingStop();
+	}
+	_viewportNext.left  = _viewport.left;
+	_viewportNext.right = _viewport.right;
+}
+
+void ESPER::scrollRight() {
+	if (_viewport.right < kPhotoWidth - 1) {
+		_flash = true;
+		setStatePhoto(kEsperPhotoStateScrolling);
+
+		_viewportNext.left   = _viewport.left  + 40;
+		_viewportNext.right  = _viewport.right + 40;
+		_viewportNext.top    = _viewport.top;
+		_viewportNext.bottom = _viewport.bottom;
+
+		if (_viewportNext.right > kPhotoWidth - 1) {
+			_viewportNext.left  -= _viewportNext.right - (kPhotoWidth - 1);
+			_viewportNext.right  = kPhotoWidth - 1;
+			scrollingStop();
+		}
+	}
+}
+
+void ESPER::scrollDown() {
+	if (_viewport.bottom < kPhotoHeight - 1) {
+		_flash = true;
+		setStatePhoto(kEsperPhotoStateScrolling);
+
+		_viewportNext.top    = _viewport.top    + 40;
+		_viewportNext.bottom = _viewport.bottom + 40;
+		_viewportNext.left   = _viewport.left;
+		_viewportNext.right  = _viewport.right;
+
+		if (_viewportNext.bottom > kPhotoHeight - 1) {
+			_viewportNext.top    -= _viewportNext.bottom - (kPhotoHeight - 1);
+			_viewportNext.bottom  = kPhotoHeight - 1;
+			scrollingStop();
+		}
+	}
+}
+
+void ESPER::goBack() {
+	// CD-changing logic has been removed
+
+	if (_stateMain == kEsperMainStateList) {
+		close();
+	} else {
+		resetData();
+		activate(false);
+	}
+}
+
+void ESPER::prepareZoom() {
+	_selectionZoomStep         = 0;
+	_timeSelectionZoomNext     = 0;
+
+	_selectionRectTarget       = _selectionRect;
+	resetSelectionRect();
+	_selectionRectDelta.left   = (_selectionRectTarget.left   - _selectionRect.left)   / kSelectionZoomSteps;
+	_selectionRectDelta.top    = (_selectionRectTarget.top    - _selectionRect.top)    / kSelectionZoomSteps;
+	_selectionRectDelta.right  = (_selectionRectTarget.right  - _selectionRect.right)  / kSelectionZoomSteps;
+	_selectionRectDelta.bottom = (_selectionRectTarget.bottom - _selectionRect.bottom) / kSelectionZoomSteps;
+
+	Common::Rect rect = _selectionRectTarget;
+	if (_regionSelectedAck) {
+		rect.left   = viewportXToScreenX(_regions[_regionSelected].rectSelection.left);
+		rect.top    = viewportYToScreenY(_regions[_regionSelected].rectSelection.top);
+		rect.right  = viewportXToScreenX(_regions[_regionSelected].rectSelection.right);
+		rect.bottom = viewportYToScreenY(_regions[_regionSelected].rectSelection.bottom);
+	}
+
+	_zoomSteps = 10;
+	float ratio = (rect.width() + 1.0f) / (float)kScreen.width();
+	if (ratio == 0.0f) {
+		_zoomTarget = ratio;
+		_zoomDelta  = 0.0f;
+	} else {
+		_zoomTarget = CLIP(_zoom / ratio, _zoomMin, 2.0f);
+		_zoomSteps  = CLIP((int)(_zoomTarget / _zoom) - 1, 0, 5) + 5;
+		_zoomDelta  = (_zoomTarget - _zoom) / (float)_zoomSteps;
+	}
+
+	_blur = 1.0f;
+
+	_viewportPositionXTarget  = _viewport.left + ((rect.left + rect.right) / 2 - kScreen.left) * _viewport.width()  / kScreen.width();
+	_viewportPositionYTarget  = _viewport.top +  ((rect.top + rect.bottom) / 2 - kScreen.top ) * _viewport.height() / kScreen.height();
+	_viewportPositionXDelta   = (_viewportPositionXTarget - _viewportPositionX) / (float)_zoomSteps;
+	_viewportPositionYDelta   = (_viewportPositionYTarget - _viewportPositionY) / (float)_zoomSteps;
+	_viewportPositionXCurrent = _viewportPositionX;
+	_viewportPositionYCurrent = _viewportPositionY;
+}
+
+void ESPER::updateSelection() {
+	int selectionWidth  = abs(_selectionRect.right + 1 - _selectionRect.left);
+	int selectionHeight = abs(_selectionRect.bottom + 1 - _selectionRect.top);
+
+	int photoSelectedWidth = _viewport.width() * selectionWidth / kScreen.width();
+	if (photoSelectedWidth < _screenHalfWidth) {
+		// minimal width of selection
+		selectionWidth = kScreen.width() * _screenHalfWidth / _viewport.width();
+	}
+
+	photoSelectedWidth = _viewport.height() * selectionHeight / kScreen.height();
+	if (photoSelectedWidth < _screenHalfHeight) {
+		// minimal height of selection
+		selectionHeight = kScreen.height() * _screenHalfHeight / _viewport.height();
+	}
+
+	// correct aspect ratio
+	if (selectionWidth / (float)kScreen.width() <= selectionHeight / (float)kScreen.height()) {
+		while (selectionWidth / (float)kScreen.width() <= selectionHeight / (float)kScreen.height()) {
+			++selectionWidth;
+		}
+	} else {
+		while (selectionHeight / (float)kScreen.height() <= selectionWidth / (float)kScreen.width()) {
+			++selectionHeight;
+		}
+	}
+
+	if (selectionWidth > kScreen.width()) {
+		selectionWidth = kScreen.width();
+	}
+	if (selectionHeight > kScreen.height()) {
+		selectionHeight =  kScreen.height();
+	}
+
+	int left   = _viewport.right  - (kScreen.right - 1     - _selectionRect.left) * _viewport.width()  / kScreen.width();
+	int right  = _viewport.left   + (_selectionRect.right  - kScreen.left       ) * _viewport.width()  / kScreen.width();
+	int top    = _viewport.bottom - (kScreen.bottom - 1    - _selectionRect.top ) * _viewport.height() / kScreen.height();
+	int bottom = _viewport.top    + (_selectionRect.bottom - kScreen.top        ) * _viewport.height() / kScreen.height();
+
+	bool stop = false;
+	bool alternate = false;
+
+	while (selectionWidth > abs(_selectionRect.right + 1 - _selectionRect.left)) {
+		if (alternate) {
+			--_selectionRect.left;
+			if (_selectionRect.left < 0) {
+				left = _viewport.right  - (kScreen.right - 1 + 100 - _selectionRect.left) * _viewport.width() / kScreen.width();
+				if (left < 0) {
+					left = 0;
+					++_selectionRect.left;
+					if (stop) {
+						break;
+					}
+					stop = true;
+					alternate = false;
+				}
+			}
+		} else {
+			++_selectionRect.right;
+			if (_selectionRect.right > kScreen.right - 1) {
+				right = _viewport.left + (_selectionRect.right - kScreen.left) * _viewport.width() / kScreen.width();
+				if (right > kPhotoWidth - 1) {
+					right = kPhotoWidth - 1;
+					--_selectionRect.right;
+					if (stop) {
+						break;
+					}
+					stop = true;
+					alternate = true;
+				}
+			}
+		}
+		if (!stop) {
+			alternate = !alternate;
+		}
+	}
+
+	alternate = false;
+	stop = false;
+	while (selectionHeight > abs(_selectionRect.bottom + 1 - _selectionRect.top)) {
+		if (alternate) {
+			--_selectionRect.top;
+			if (_selectionRect.top < 0) {
+				top = _viewport.bottom - (kScreen.bottom - 1 - _selectionRect.top) * _viewport.height() / kScreen.height();
+				if (top < 0) {
+					top = 0;
+					++_selectionRect.top;
+					if (stop) {
+						break;
+					}
+					stop = true;
+					alternate = false;
+				}
+			}
+		} else {
+			++_selectionRect.bottom;
+			if (_selectionRect.bottom > kScreen.bottom - 1)	{
+				bottom = _viewport.top + (_selectionRect.bottom - kScreen.top) * _viewport.height() / kScreen.height();
+				if (bottom > kPhotoHeight - 1) {
+					bottom = kPhotoHeight - 1;
+					--_selectionRect.bottom;
+					if (stop) {
+						break;
+					}
+					alternate = true;
+					stop = true;
+				}
+			}
+		}
+		if (!stop) {
+			alternate = !alternate;
+		}
+	}
+
+	_regionSelected = findRegion(Common::Rect(left, top, right, bottom));
+	if (_regionSelected >= 0) {
+		_regionSelectedAck = true;
+		setStatePhoto(kEsperPhotoStatePhotoZooming);
+	}
+}
+
+int ESPER::viewportXToScreenX(int x) {
+	return kScreen.width() * (x - _viewport.left) / _viewport.width() + kScreen.left;
+}
+
+int ESPER::viewportYToScreenY(int y) {
+	return kScreen.height() * (y - _viewport.top) / _viewport.height() + kScreen.top;
+}
+
+} // End of namespace BladeRunner
diff --git a/engines/bladerunner/ui/esper.h b/engines/bladerunner/ui/esper.h
new file mode 100644
index 0000000..9d0dd6d
--- /dev/null
+++ b/engines/bladerunner/ui/esper.h
@@ -0,0 +1,285 @@
+/* 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 2
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef BLADERUNNER_ESPER_H
+#define BLADERUNNER_ESPER_H
+
+#include "common/array.h"
+#include "common/rect.h"
+
+namespace Graphics {
+struct Surface;
+}
+
+namespace BladeRunner {
+
+class BladeRunnerEngine;
+class Font;
+class Shape;
+class VQAPlayer;
+class UIImagePicker;
+class ESPERScript;
+
+// CD-changing logic has been removed
+
+enum EsperMainStates {
+	kEsperMainStateOpening      = 0,
+	kEsperMainStateList         = 1,
+	kEsperMainStatePhotoOpening = 2,
+	kEsperMainStateClear        = 3,
+	kEsperMainStatePhoto        = 5
+};
+
+enum EsperPhotoStates {
+	kEsperPhotoStateShow              = 0,
+	kEsperPhotoStateOpening           = 1,
+	kEsperPhotoStateScrolling         = 2,
+	kEsperPhotoStateSelectionZooming  = 3,
+	kEsperPhotoStateSelectionBlinking = 4,
+	kEsperPhotoStatePhotoZooming      = 5,
+	kEsperPhotoStatePhotoSharpening   = 6,
+	kEsperPhotoStatePhotoZoomOut      = 7,
+	kEsperPhotoStateVideoZooming      = 8,
+	kEsperPhotoStateVideoShow         = 9,
+	kEsperPhotoStateVideoZoomOut      = 10
+};
+
+class ESPER {
+	static const int kPhotoCount         = 12;
+	static const int kRegionCount        = 6;
+	static const int kPhotoWidth         = 1280;
+	static const int kPhotoHeight        = 960;
+	static const int kSelectionZoomSteps = 6;
+
+	static const Common::Rect kScreen;
+
+	struct Photo {
+		bool isPresent;
+		char name[13];
+		int photoId;
+		int shapeId;
+	};
+
+	struct Region {
+		bool isPresent;
+		int regionId;
+		Common::Rect rectInner;
+		Common::Rect rectOuter;
+		Common::Rect rectSelection;
+		char name[13];
+	};
+
+	BladeRunnerEngine     *_vm;
+	ESPERScript           *_script;
+
+	bool _isWaiting;
+	bool _isOpen;
+
+	UIImagePicker     *_buttons;
+
+	Graphics::Surface *_photoData;
+	Graphics::Surface *_viewportData;
+
+	VQAPlayer *_vqaMainPlayer;
+	VQAPlayer *_vqaPhotoPlayer;
+	int        _vqaLastFrame;
+
+	Shape                 *_shapeButton;
+	Common::Array<Shape *> _shapesPhotos;
+	Shape                 *_shapeThumbnail;
+
+	Photo _photos[kPhotoCount];
+	int   _photoIdSelected;
+
+	Region _regions[kRegionCount];
+	int    _regionSelected;
+	bool   _regionSelectedAck;
+
+	EsperMainStates  _stateMain;
+	EsperPhotoStates _statePhoto;
+
+	bool _isDrawingSelection;
+	bool _isMouseDown;
+	int  _mouseOverScroll;
+
+	float _zoomHorizontal;
+	float _zoomVertical;
+	float _zoom;
+	float _zoomMin;
+	float _zoomTarget;
+	float _zoomDelta;
+	float _blur;
+	int   _zoomSteps;
+	int   _zoomStep;
+	int   _timeZoomNext;
+
+	bool _isZoomingOut;
+	int  _timeZoomOutNext;
+
+	Common::Rect _viewport;
+	Common::Rect _viewportNext;
+	int          _viewportPositionX;
+	int          _viewportPositionY;
+	float        _viewportPositionXCurrent;
+	float        _viewportPositionYCurrent;
+	int          _viewportPositionXTarget;
+	int          _viewportPositionYTarget;
+	float        _viewportPositionXDelta;
+	float        _viewportPositionYDelta;
+	int          _viewportWidth;
+	int          _viewportHeight;
+
+	int _screenHalfWidth;
+	int _screenHalfHeight;
+
+	int _flash;
+
+	Common::Rect _selectionRect;
+	Common::Rect _selectionRectTarget;
+	Common::Rect _selectionRectDelta;
+	int          _selectionCrosshairX;
+	int          _selectionCrosshairY;
+
+	int _selectionBlinkingCounter;
+	int _selectionBlinkingStyle;
+	int _timeSelectionBlinkingNext;
+
+	int _selectionZoomStep;
+	int _timeSelectionZoomNext;
+
+	int _photoOpeningWidth;
+	int _photoOpeningHeight;
+	int _timePhotoOpeningNext;
+
+	bool _isScrolling;
+	int  _scrollingDirection;
+	int  _timeScrollNext;
+
+	int _soundId1;
+	int _volume1;
+	int _soundId2;
+	int _volume2;
+	int _soundId3;
+	int _volume3;
+	int _ambientVolume;
+
+public:
+	ESPER(BladeRunnerEngine *vm);
+	~ESPER();
+
+	void open(Graphics::Surface *surface);
+	void close();
+	bool isOpen();
+
+	void handleMouseUp(int x, int y, bool buttonLeft);
+	void handleMouseDown(int x, int y, bool buttonLeft);
+
+	void tick();
+
+	void resume();
+
+	void addPhoto(const char *name, int photoId, int shapeId);
+	void defineRegion(int regionId, Common::Rect inner, Common::Rect outer, Common::Rect selection, const char *name);
+
+private:
+	static void mouseDownCallback(int, void *);
+	static void mouseUpCallback(int, void *);
+
+	void reset();
+	void resetData();
+	void resetPhotos();
+	void resetRegions();
+	void resetViewport();
+	void resetSelectionRect();
+	void resetSelectionBlinking();
+	void resetPhotoZooming();
+	void resetPhotoOpening();
+
+	void updateViewport();
+
+	void activate(bool withOpening);
+	void setStateMain(EsperMainStates state);
+	void setStatePhoto(EsperPhotoStates state);
+
+	void wait(int timeout);
+	void playSound(int soundId, int volume);
+
+	void draw(Graphics::Surface &surface);
+
+	void drawPhotoOpening(Graphics::Surface &surface);
+	bool drawSelectionZooming(Graphics::Surface &surface);
+	bool drawSelectionBlinking(Graphics::Surface &surface);
+	void drawPhotoZooming(Graphics::Surface &surface);
+	void drawPhotoSharpening(Graphics::Surface &surface);
+	void drawPhotoZoomOut(Graphics::Surface &surface);
+	void drawVideoZooming(Graphics::Surface &surface);
+	void drawVideoZoomOut(Graphics::Surface &surface);
+
+	void drawPhoto(Graphics::Surface &surface);
+	void drawGrid(Graphics::Surface &surface);
+	void drawPhotoWithGrid(Graphics::Surface &surface);
+	void drawSelection(Graphics::Surface &surface, bool crosshair, int style);
+	void drawVideoFrame(Graphics::Surface &surface);
+	void drawTextCoords(Graphics::Surface &surface);
+
+	void flashViewport();
+
+	void copyImageScale(Graphics::Surface *src, Common::Rect srcRect, Graphics::Surface *dst, Common::Rect dstRect);
+	void copyImageBlur(Graphics::Surface *src, Common::Rect srcRect, Graphics::Surface *dst, Common::Rect dstRect, float u);
+	void copyImageBlit(Graphics::Surface *src, Common::Rect srcRect, Graphics::Surface *dst, Common::Rect dstRect);
+
+	void tickSound();
+	void tickMouse(Graphics::Surface &surface);
+	void tickScroll();
+
+	int findEmptyPhoto();
+	void selectPhoto(int photoId);
+	void unloadPhotos();
+
+	int findEmptyRegion();
+	int findRegion(Common::Rect where);
+
+	void zoomingStart();
+	void zoomOutStart();
+	void zoomOutStop();
+
+	void scrollingStart(int direction);
+	void scrollingStop();
+	void scrollUpdate();
+	void scrollLeft();
+	void scrollUp();
+	void scrollRight();
+	void scrollDown();
+
+	void goBack();
+
+	void prepareZoom();
+	void updateSelection();
+
+	int viewportXToScreenX(int x);
+	int viewportYToScreenY(int y);
+
+};
+
+} // End of namespace BladeRunner
+
+#endif
diff --git a/engines/bladerunner/ui/kia.cpp b/engines/bladerunner/ui/kia.cpp
index 95a66b6..7563816 100644
--- a/engines/bladerunner/ui/kia.cpp
+++ b/engines/bladerunner/ui/kia.cpp
@@ -59,20 +59,6 @@ namespace BladeRunner {
 
 const char *KIA::kPogo = "POGO";
 
-enum KIASections {
-	kKIASectionNone = 0,
-	kKIASectionCrimes = 1,
-	kKIASectionSuspects = 2,
-	kKIASectionClues = 3,
-	kKIASectionSettings = 4,
-	kKIASectionHelp = 5,
-	kKIASectionSave = 6,
-	kKIASectionLoad = 7,
-	kKIASectionQuit = 8,
-	kKIASectionDiagnostic = 9,
-	kKIASectionPogo = 10
-};
-
 KIA::KIA(BladeRunnerEngine *vm) {
 	_vm = vm;
 
@@ -137,12 +123,81 @@ void KIA::openLastOpened() {
 	open(_lastSectionIdKIA);
 }
 
-void KIA::openOptions() {
-	open(kKIASectionSettings);
+void KIA::open(KIASections sectionId) {
+	if (_currentSectionId == sectionId) {
+		return;
+	}
+
+	if (!sectionId) {
+		unload();
+		return;
+	}
+
+	if (!isOpen()) {
+		init();
+	}
+
+	switch (_currentSectionId) {
+		case kKIASectionCrimes:
+			_crimesSection->saveToLog();
+			break;
+		case kKIASectionSuspects:
+			_suspectsSection->saveToLog();
+			break;
+		case kKIASectionClues:
+			_cluesSection->saveToLog();
+			break;
+		default:
+			break;
+	}
+
+	if (sectionId != kKIASectionCrimes && sectionId != kKIASectionSuspects && sectionId != kKIASectionClues) {
+		playerReset();
+	}
+
+	_transitionId = getTransitionId(_currentSectionId, sectionId);
+	const char *name = getSectionVqaName(sectionId);
+	if (getSectionVqaName(_currentSectionId) != name) {
+		if (_mainVqaPlayer) {
+			_mainVqaPlayer->close();
+			delete _mainVqaPlayer;
+			_mainVqaPlayer = nullptr;
+		}
+
+		_mainVqaPlayer = new VQAPlayer(_vm, &_vm->_surfaceBack);
+		_mainVqaPlayer->open(name);
+	}
+
+	if (_transitionId) {
+		playTransitionSound(_transitionId);
+		_mainVqaPlayer->setLoop(getVqaLoopTransition(_transitionId), -1, kLoopSetModeImmediate, nullptr, nullptr);
+		_mainVqaPlayer->setLoop(getVqaLoopMain(sectionId), -1, kLoopSetModeEnqueue, loopEnded, this);
+	} else {
+		int loopId = getVqaLoopMain(sectionId);
+		_mainVqaPlayer->setLoop(loopId, -1, kLoopSetModeImmediate, nullptr, nullptr);
+		_mainVqaPlayer->setLoop(loopId + 1, -1, kLoopSetModeJustStart, nullptr, nullptr);
+	}
+
+	_buttons->resetImages();
+	createButtons(sectionId);
+	switchSection(sectionId);
+	_currentSectionId = sectionId;
+
+	if (sectionId == kKIASectionCrimes || sectionId == kKIASectionSuspects || sectionId == kKIASectionClues) {
+		_lastSectionIdKIA = _currentSectionId;
+	}
+
+	if (sectionId == kKIASectionSettings || sectionId == kKIASectionHelp || sectionId == kKIASectionSave || sectionId == kKIASectionLoad) {
+		 _lastSectionIdOptions = _currentSectionId;
+	}
+}
+
+bool KIA::isOpen() {
+	return _currentSectionId != kKIASectionNone;
 }
 
 void KIA::tick() {
-	if (!_currentSectionId) {
+	if (!isOpen()) {
 		return;
 	}
 
@@ -235,8 +290,9 @@ void KIA::tick() {
 			_shapes->get(39)->draw(_vm->_surfaceFront, 583, 342);
 		}
 	}
+	//TODO: implement frame loading after seek, then advanceFrame can be removed
 	_playerVqaPlayer->seekToFrame(_playerVqaFrame);
-	_playerVqaPlayer->update(true); //_vm->_surfaceFront, 3
+	_playerVqaPlayer->update(true, true);
 
 	_playerSliceModelAngle += (float)(timeDiff) * 1.0f/400.0f;
 	while (_playerSliceModelAngle >= 2 * M_PI) {
@@ -296,11 +352,24 @@ void KIA::tick() {
 	_vm->_mouse->draw(_vm->_surfaceFront, mouse.x, mouse.y);
 	_vm->blitToScreen(_vm->_surfaceFront);
 
+	_vm->_system->delayMillis(10);
+
 	_timeLast = timeNow;
 }
 
+void KIA::resume() {
+	// vqaPlayer::clear(this->_vqaPlayerMain);
+	if (_transitionId) {
+		_mainVqaPlayer->setLoop(getVqaLoopTransition(_transitionId), -1, kLoopSetModeImmediate, nullptr, nullptr);
+		_mainVqaPlayer->setLoop(getVqaLoopMain(_currentSectionId), -1, kLoopSetModeEnqueue, loopEnded, this);
+	} else {
+		_mainVqaPlayer->setLoop(getVqaLoopMain(_currentSectionId), -1, kLoopSetModeImmediate, nullptr, nullptr);
+		_mainVqaPlayer->setLoop(getVqaLoopMain(_currentSectionId) + 1, -1, kLoopSetModeJustStart, nullptr, nullptr);
+	}
+}
+
 void KIA::handleMouseDown(int mouseX, int mouseY, bool mainButton) {
-	if (!_currentSectionId) {
+	if (!isOpen()) {
 		return;
 	}
 	if (mainButton) {
@@ -312,7 +381,7 @@ void KIA::handleMouseDown(int mouseX, int mouseY, bool mainButton) {
 }
 
 void KIA::handleMouseUp(int mouseX, int mouseY, bool mainButton) {
-	if (!_currentSectionId) {
+	if (!isOpen()) {
 		return;
 	}
 	if (mainButton) {
@@ -339,7 +408,7 @@ void KIA::handleMouseUp(int mouseX, int mouseY, bool mainButton) {
 }
 
 void KIA::handleKeyUp(const Common::KeyState &kbd) {
-	if (!_currentSectionId) {
+	if (!isOpen()) {
 		return;
 	}
 
@@ -370,7 +439,7 @@ void KIA::handleKeyUp(const Common::KeyState &kbd) {
 }
 
 void KIA::handleKeyDown(const Common::KeyState &kbd) {
-	if (!_currentSectionId) {
+	if (!isOpen()) {
 		return;
 	}
 	switch (kbd.keycode) {
@@ -473,7 +542,7 @@ void KIA::playPhotograph(int photographId) {
 	}
 	_playerPhotographId = photographId;
 	_playerPhotograph = new Shape(_vm);
-	_playerPhotograph->readFromContainer("photos.shp", photographId);
+	_playerPhotograph->open("photos.shp", photographId);
 }
 
 void KIA::mouseDownCallback(int buttonId, void *callbackData) {
@@ -544,73 +613,6 @@ void KIA::loopEnded(void *callbackData, int frame, int loopId) {
 	self->_transitionId = 0;
 }
 
-void KIA::open(int sectionId) {
-	if (_currentSectionId == sectionId) {
-		return;
-	}
-
-	if (!sectionId) {
-		unload();
-		return;
-	}
-
-	if (!_currentSectionId) {
-		init();
-	}
-
-	switch (_currentSectionId) {
-		case kKIASectionCrimes:
-			_crimesSection->saveToLog();
-			break;
-		case kKIASectionSuspects:
-			_suspectsSection->saveToLog();
-			break;
-		case kKIASectionClues:
-			_cluesSection->saveToLog();
-			break;
-	}
-
-	if (sectionId != kKIASectionCrimes && sectionId != kKIASectionSuspects && sectionId != kKIASectionClues) {
-		playerReset();
-	}
-
-	_transitionId = getTransitionId(_currentSectionId, sectionId);
-	const char *name = getSectionVqaName(sectionId);
-	if (getSectionVqaName(_currentSectionId) != name) {
-		if (_mainVqaPlayer) {
-			_mainVqaPlayer->close();
-			delete _mainVqaPlayer;
-			_mainVqaPlayer = nullptr;
-		}
-
-		_mainVqaPlayer = new VQAPlayer(_vm, &_vm->_surfaceBack);
-		_mainVqaPlayer->open(name);
-	}
-
-	if (_transitionId) {
-		playTransitionSound(_transitionId);
-		_mainVqaPlayer->setLoop(getVqaLoopTransition(_transitionId), -1, kLoopSetModeImmediate, nullptr, nullptr);
-		_mainVqaPlayer->setLoop(getVqaLoopMain(sectionId), -1, kLoopSetModeEnqueue, loopEnded, this);
-	} else {
-		int loopId = getVqaLoopMain(sectionId);
-		_mainVqaPlayer->setLoop(loopId, -1, kLoopSetModeImmediate, nullptr, nullptr);
-		_mainVqaPlayer->setLoop(loopId + 1, -1, kLoopSetModeJustStart, nullptr, nullptr);
-	}
-
-	_buttons->resetImages();
-	createButtons(sectionId);
-	switchSection(sectionId);
-	_currentSectionId = sectionId;
-
-	if (sectionId == kKIASectionCrimes || sectionId == kKIASectionSuspects || sectionId == kKIASectionClues) {
-		_lastSectionIdKIA = _currentSectionId;
-	}
-
-	if (sectionId == kKIASectionSettings || sectionId == kKIASectionHelp || sectionId == kKIASectionSave || sectionId == kKIASectionLoad) {
-		 _lastSectionIdOptions = _currentSectionId;
-	}
-}
-
 void KIA::init() {
 	if (!_vm->openArchive("MODE.MIX")) {
 		return;
@@ -641,7 +643,7 @@ void KIA::init() {
 }
 
 void KIA::unload() {
-	if (!_currentSectionId) {
+	if (!isOpen()) {
 		return;
 	}
 
@@ -671,7 +673,7 @@ void KIA::unload() {
 
 	_vm->closeArchive("MODE.MIX");
 
-	_currentSectionId = 0;
+	_currentSectionId = kKIASectionNone;
 
 	// TODO: Unfreeze game time
 
@@ -846,7 +848,7 @@ void KIA::createButtons(int sectionId) {
 void KIA::buttonClicked(int buttonId) {
 	int soundId = 0;
 
-	if (!_currentSectionId) {
+	if (!isOpen()) {
 		return;
 	}
 	switch (buttonId) {
diff --git a/engines/bladerunner/ui/kia.h b/engines/bladerunner/ui/kia.h
index 0452230..0ac7a22 100644
--- a/engines/bladerunner/ui/kia.h
+++ b/engines/bladerunner/ui/kia.h
@@ -54,6 +54,21 @@ class Shape;
 class UIImagePicker;
 class VQAPlayer;
 
+enum KIASections {
+	kKIASectionNone       = 0,
+	kKIASectionCrimes     = 1,
+	kKIASectionSuspects   = 2,
+	kKIASectionClues      = 3,
+	kKIASectionSettings   = 4,
+	kKIASectionHelp       = 5,
+	kKIASectionSave       = 6,
+	kKIASectionLoad       = 7,
+	kKIASectionQuit       = 8,
+	kKIASectionDiagnostic = 9,
+	kKIASectionPogo       = 10
+};
+
+
 class KIA {
 	static const char *kPogo;
 	static const int kPlayerActorDialogueQueueCapacity = 31;
@@ -68,9 +83,6 @@ class KIA {
 	int _forceOpen;
 	int _transitionId;
 
-	int _lastSectionIdKIA;
-	int _lastSectionIdOptions;
-
 	int        _playerVqaTimeLast;
 	VQAPlayer *_playerVqaPlayer;
 	int        _playerVqaFrame;
@@ -86,7 +98,11 @@ class KIA {
 	int                     _playerActorDialogueQueueSize;
 	int                     _playerActorDialogueState;
 
+	KIASections           _currentSectionId;
+	KIASections           _lastSectionIdKIA;
+	KIASections           _lastSectionIdOptions;
 	KIASectionBase       *_currentSection;
+
 	KIASectionClues      *_cluesSection;
 	KIASectionCrimes     *_crimesSection;
 	KIASectionDiagnostic *_diagnosticSection;
@@ -104,20 +120,22 @@ class KIA {
 	int                   _pogoPos;
 
 public:
-	int        _currentSectionId;
-	KIALog    *_log;
-	KIAScript *_script;
-	KIAShapes *_shapes;
+	KIALog     *_log;
+	KIAScript  *_script;
+	KIAShapes  *_shapes;
 
 public:
 	KIA(BladeRunnerEngine *vm);
 	~KIA();
 
 	void openLastOpened();
-	void openOptions();
+	void open(KIASections sectionId);
+	bool isOpen();
 
 	void tick();
 
+	void resume();
+
 	void handleMouseDown(int mouseX, int mouseY, bool mainButton);
 	void handleMouseUp(int mouseX, int mouseY, bool mainButton);
 	void handleKeyUp(const Common::KeyState &kbd);
@@ -133,7 +151,6 @@ private:
 	static void mouseUpCallback(int buttonId, void *callbackData);
 	static void loopEnded(void *callbackData, int frame, int loopId);
 
-	void open(int sectionId);
 	void init();
 	void unload();
 	void switchSection(int sectionId);
diff --git a/engines/bladerunner/ui/kia_section_crimes.cpp b/engines/bladerunner/ui/kia_section_crimes.cpp
index b0b3d7c..a5ff221 100644
--- a/engines/bladerunner/ui/kia_section_crimes.cpp
+++ b/engines/bladerunner/ui/kia_section_crimes.cpp
@@ -409,7 +409,7 @@ void KIASectionCrimes::updateSuspectPhoto() {
 
 	if (_suspectPhotoShapeId != -1) {
 		_suspectPhotoShape = new Shape(_vm);
-		_suspectPhotoShape->readFromContainer("photos.shp", _suspectPhotoShapeId);
+		_suspectPhotoShape->open("photos.shp", _suspectPhotoShapeId);
 	}
 }
 
diff --git a/engines/bladerunner/ui/kia_section_suspects.cpp b/engines/bladerunner/ui/kia_section_suspects.cpp
index ce13243..053705c 100644
--- a/engines/bladerunner/ui/kia_section_suspects.cpp
+++ b/engines/bladerunner/ui/kia_section_suspects.cpp
@@ -498,7 +498,7 @@ void KIASectionSuspects::updateSuspectPhoto() {
 
 	if (_suspectPhotoShapeId != -1) {
 		_suspectPhotoShape = new Shape(_vm);
-		_suspectPhotoShape->readFromContainer("photos.shp", _suspectPhotoShapeId);
+		_suspectPhotoShape->open("photos.shp", _suspectPhotoShapeId);
 	}
 }
 
diff --git a/engines/bladerunner/ui/kia_shapes.cpp b/engines/bladerunner/ui/kia_shapes.cpp
index d992ec7..ce5c8a7 100644
--- a/engines/bladerunner/ui/kia_shapes.cpp
+++ b/engines/bladerunner/ui/kia_shapes.cpp
@@ -45,7 +45,7 @@ void KIAShapes::load() {
 
 	for (uint i = 0; i < kShapeCount; ++i) {
 		Shape *shape = new Shape(_vm);
-		shape->readFromContainer("kiaopt.shp", i);
+		shape->open("kiaopt.shp", i);
 		_shapes[i] = shape;
 	}
 
diff --git a/engines/bladerunner/ui/spinner.cpp b/engines/bladerunner/ui/spinner.cpp
index 72c8b19..2df9390 100644
--- a/engines/bladerunner/ui/spinner.cpp
+++ b/engines/bladerunner/ui/spinner.cpp
@@ -23,6 +23,7 @@
 #include "bladerunner/ui/spinner.h"
 
 #include "bladerunner/bladerunner.h"
+#include "bladerunner/game_constants.h"
 #include "bladerunner/mouse.h"
 #include "bladerunner/scene.h"
 #include "bladerunner/shape.h"
@@ -123,7 +124,7 @@ int Spinner::chooseDestination(int loopId, bool immediately) {
 
 	for (int j = 0; j != shapeCount; ++j) {
 		_shapes.push_back(new Shape(_vm));
-		_shapes[j]->readFromContainer("SPINNER.SHP", firstShapeId + j);
+		_shapes[j]->open("SPINNER.SHP", firstShapeId + j);
 	}
 
 	_imagePicker->resetImages();
diff --git a/engines/bladerunner/ui/ui_image_picker.cpp b/engines/bladerunner/ui/ui_image_picker.cpp
index 6c46880..ca61a4d 100644
--- a/engines/bladerunner/ui/ui_image_picker.cpp
+++ b/engines/bladerunner/ui/ui_image_picker.cpp
@@ -24,6 +24,7 @@
 
 #include "bladerunner/bladerunner.h"
 #include "bladerunner/font.h"
+#include "bladerunner/game_constants.h"
 #include "bladerunner/mouse.h"
 #include "bladerunner/shape.h"
 
@@ -207,6 +208,7 @@ void UIImagePicker::draw(Graphics::Surface &surface) {
 		}
 #if BLADERUNNER_DEBUG_RENDERING
 		surface.frameRect(img.rect, 0x7fff);
+		_vm->_mainFont->drawColor(Common::String::format("%d", i), surface, (img.rect.left + img.rect.right) / 2, (img.rect.top + img.rect.bottom) / 2, 0x7fff);
 #endif
 	}
 }
@@ -252,11 +254,11 @@ void UIImagePicker::drawTooltip(Graphics::Surface &surface, int x, int y) {
 	_vm->_mainFont->drawColor(tooltip, surface, rect.left + 2, rect.top, 0x7FFF);
 }
 
-void UIImagePicker::handleMouseAction(int x, int y, bool down, bool up, bool ignore) {
+bool UIImagePicker::handleMouseAction(int x, int y, bool down, bool up, bool ignore) {
 	if (!_isVisible || ignore) {
-		return;
+		return false;
 	}
-
+	bool actionHandled = false;
 	int hoveredImageIndex = -1;
 	for (int i = 0; i != _imageCount; ++i) {
 		if (_images[i].rect.contains(x, y)) {
@@ -286,9 +288,10 @@ void UIImagePicker::handleMouseAction(int x, int y, bool down, bool up, bool ign
 	if (down && !_isButtonDown) {
 		_isButtonDown = true;
 		_pressedImageIndex = _hoveredImageIndex;
-		if (_hoveredImageIndex != 1) {
+		if (_hoveredImageIndex != -1) {
 			if (_mouseDownCallback) {
 				_mouseDownCallback(_hoveredImageIndex, _callbackData);
+				actionHandled = true;
 			}
 		}
 	}
@@ -297,13 +300,17 @@ void UIImagePicker::handleMouseAction(int x, int y, bool down, bool up, bool ign
 	if (up) {
 		if (_isButtonDown) {
 			if (_hoveredImageIndex == _pressedImageIndex && _pressedImageIndex != -1) {
-				if (_mouseUpCallback)
+				if (_mouseUpCallback) {
 					_mouseUpCallback(_hoveredImageIndex, _callbackData);
+					actionHandled = true;
+				}
 			}
 		}
 		_isButtonDown = false;
 		_pressedImageIndex = -1;
 	}
+
+	return actionHandled;
 }
 
 void UIImagePicker::resetImage(int i) {
diff --git a/engines/bladerunner/ui/ui_image_picker.h b/engines/bladerunner/ui/ui_image_picker.h
index 00f2f3d..d5b18b2 100644
--- a/engines/bladerunner/ui/ui_image_picker.h
+++ b/engines/bladerunner/ui/ui_image_picker.h
@@ -91,7 +91,7 @@ public:
 	void draw(Graphics::Surface &surface);
 	void drawTooltip(Graphics::Surface &surface, int x, int y);
 
-	void handleMouseAction(int x, int y, bool down, bool up, bool ignore = false);
+	bool handleMouseAction(int x, int y, bool down, bool up, bool ignore = false);
 
 	void resetImage(int i);
 	bool hasHoveredImage();
diff --git a/engines/bladerunner/vqa_decoder.cpp b/engines/bladerunner/vqa_decoder.cpp
index aec4d18..7dc7616 100644
--- a/engines/bladerunner/vqa_decoder.cpp
+++ b/engines/bladerunner/vqa_decoder.cpp
@@ -25,6 +25,7 @@
 #include "bladerunner/bladerunner.h"
 #include "bladerunner/decompress_lcw.h"
 #include "bladerunner/decompress_lzo.h"
+#include "bladerunner/game_info.h"
 #include "bladerunner/lights.h"
 #include "bladerunner/screen_effects.h"
 #include "bladerunner/view.h"
@@ -116,9 +117,8 @@ const char *strTag(uint32 tag) {
 	return s;
 }
 
-VQADecoder::VQADecoder(Graphics::Surface *surface) :
+VQADecoder::VQADecoder() :
 	  _s(nullptr),
-	  _surface(surface),
 	  _frameInfo(nullptr),
 	  _videoTrack(nullptr),
 	  _audioTrack(nullptr),
@@ -179,7 +179,7 @@ bool VQADecoder::loadStream(Common::SeekableReadStream *s) {
 		}
 	} while (chd.id != kFINF);
 
-	_videoTrack = new VQAVideoTrack(this, _surface);
+	_videoTrack = new VQAVideoTrack(this);
 	_audioTrack = new VQAAudioTrack(this);
 
 #if BLADERUNNER_DEBUG_CONSOLE
@@ -194,9 +194,9 @@ bool VQADecoder::loadStream(Common::SeekableReadStream *s) {
 	return true;
 }
 
-void VQADecoder::decodeVideoFrame(int frame, bool forceDraw) {
+void VQADecoder::decodeVideoFrame(Graphics::Surface *surface, int frame, bool forceDraw) {
 	_decodingFrame = frame;
-	_videoTrack->decodeVideoFrame(forceDraw);
+	_videoTrack->decodeVideoFrame(surface, forceDraw);
 }
 
 void VQADecoder::decodeZBuffer(ZBuffer *zbuffer) {
@@ -573,9 +573,8 @@ bool VQADecoder::readMFCI(Common::SeekableReadStream *s, uint32 size) {
 	return true;
 }
 
-VQADecoder::VQAVideoTrack::VQAVideoTrack(VQADecoder *vqaDecoder, Graphics::Surface *surface) {
+VQADecoder::VQAVideoTrack::VQAVideoTrack(VQADecoder *vqaDecoder) {
 	_vqaDecoder = vqaDecoder;
-	_surface = surface;
 	_hasNewFrame = false;
 
 	VQADecoder::Header *header = &vqaDecoder->_header;
@@ -635,9 +634,10 @@ Common::Rational VQADecoder::VQAVideoTrack::getFrameRate() const {
 	return _frameRate;
 }
 
-void VQADecoder::VQAVideoTrack::decodeVideoFrame(bool forceDraw) {
+void VQADecoder::VQAVideoTrack::decodeVideoFrame(Graphics::Surface *surface, bool forceDraw) {
 	if (_hasNewFrame || forceDraw) {
-		decodeFrame((uint16 *)_surface->getPixels());
+		assert(surface);
+		decodeFrame(surface);
 		_hasNewFrame = false;
 	}
 }
@@ -807,11 +807,12 @@ bool VQADecoder::VQAVideoTrack::readVPTR(Common::SeekableReadStream *s, uint32 s
 	return true;
 }
 
-void VQADecoder::VQAVideoTrack::VPTRWriteBlock(uint16 *frame, unsigned int dstBlock, unsigned int srcBlock, int count, bool alpha) {
-	uint16 frame_width  = _width;
-	uint32 frame_stride = 640;
-	uint16 block_width  = _blockW;
-	uint16 block_height = _blockH;
+void VQADecoder::VQAVideoTrack::VPTRWriteBlock(Graphics::Surface *surface, unsigned int dstBlock, unsigned int srcBlock, int count, bool alpha) {
+	uint16 *frame        = (uint16 *)surface->getPixels();
+	uint16  frame_width  = _width;
+	uint32  frame_stride = surface->w;
+	uint16  block_width  = _blockW;
+	uint16  block_height = _blockH;
 
 	const uint8 *const block_src =
 		&_codebook[2 * srcBlock * block_width * block_height];
@@ -845,7 +846,7 @@ void VQADecoder::VQAVideoTrack::VPTRWriteBlock(uint16 *frame, unsigned int dstBl
 	} while (--count);
 }
 
-bool VQADecoder::VQAVideoTrack::decodeFrame(uint16 *frame) {
+bool VQADecoder::VQAVideoTrack::decodeFrame(Graphics::Surface *surface) {
 	CodebookInfo &codebookInfo = _vqaDecoder->codebookInfoForFrame(_vqaDecoder->_decodingFrame);
 
 	if (!codebookInfo.data) {
@@ -876,19 +877,19 @@ bool VQADecoder::VQAVideoTrack::decodeFrame(uint16 *frame) {
 			count = 2 * (((command >> 8) & 0x1f) + 1);
 			srcBlock = command & 0x00ff;
 
-			VPTRWriteBlock(frame, dstBlock, srcBlock, count);
+			VPTRWriteBlock(surface, dstBlock, srcBlock, count);
 			dstBlock += count;
 			break;
 		case 2:
 			count = 2 * (((command >> 8) & 0x1f) + 1);
 			srcBlock = command & 0x00ff;
 
-			VPTRWriteBlock(frame, dstBlock, srcBlock, 1);
+			VPTRWriteBlock(surface, dstBlock, srcBlock, 1);
 			++dstBlock;
 
 			for (int i = 0; i < count; ++i) {
 				srcBlock = *src++;
-				VPTRWriteBlock(frame, dstBlock, srcBlock, 1);
+				VPTRWriteBlock(surface, dstBlock, srcBlock, 1);
 				++dstBlock;
 			}
 			break;
@@ -897,7 +898,7 @@ bool VQADecoder::VQAVideoTrack::decodeFrame(uint16 *frame) {
 			count = 1;
 			srcBlock = command & 0x1fff;
 
-			VPTRWriteBlock(frame, dstBlock, srcBlock, count, prefix == 4);
+			VPTRWriteBlock(surface, dstBlock, srcBlock, count, prefix == 4);
 			++dstBlock;
 			break;
 		case 5:
@@ -905,7 +906,7 @@ bool VQADecoder::VQAVideoTrack::decodeFrame(uint16 *frame) {
 			count = *src++;
 			srcBlock = command & 0x1fff;
 
-			VPTRWriteBlock(frame, dstBlock, srcBlock, count, prefix == 6);
+			VPTRWriteBlock(surface, dstBlock, srcBlock, count, prefix == 6);
 			dstBlock += count;
 			break;
 		default:
diff --git a/engines/bladerunner/vqa_decoder.h b/engines/bladerunner/vqa_decoder.h
index 8ab6be5..fdc5b53 100644
--- a/engines/bladerunner/vqa_decoder.h
+++ b/engines/bladerunner/vqa_decoder.h
@@ -54,14 +54,14 @@ enum VQADecoderSkipFlags {
 
 class VQADecoder {
 public:
-	VQADecoder(Graphics::Surface *surface);
+	VQADecoder();
 	~VQADecoder();
 
 	bool loadStream(Common::SeekableReadStream *s);
 
 	void readFrame(int frame, uint readFlags = kVQAReadAll);
 
-	void                        decodeVideoFrame(int frame, bool forceDraw = false);
+	void                        decodeVideoFrame(Graphics::Surface *surface, int frame, bool forceDraw = false);
 	void                        decodeZBuffer(ZBuffer *zbuffer);
 	Audio::SeekableAudioStream *decodeAudioFrame();
 	void                        decodeView(View *view);
@@ -139,7 +139,7 @@ private:
 	class VQAAudioTrack;
 
 	Common::SeekableReadStream *_s;
-	Graphics::Surface *_surface;
+	// Graphics::Surface *_surface;
 
 	Header   _header;
 	int      _readingFrame;
@@ -172,7 +172,7 @@ private:
 
 	class VQAVideoTrack {
 	public:
-		VQAVideoTrack(VQADecoder *vqaDecoder, Graphics::Surface *surface);
+		VQAVideoTrack(VQADecoder *vqaDecoder);
 		~VQAVideoTrack();
 
 		uint16 getWidth() const;
@@ -180,7 +180,7 @@ private:
 
 		int getFrameCount() const;
 
-		void decodeVideoFrame(bool forceDraw);
+		void decodeVideoFrame(Graphics::Surface *surface, bool forceDraw);
 		void decodeZBuffer(ZBuffer *zbuffer);
 		void decodeView(View *view);
 		void decodeScreenEffects(ScreenEffects *aesc);
@@ -202,7 +202,7 @@ private:
 
 	private:
 		VQADecoder        *_vqaDecoder;
-		Graphics::Surface *_surface;
+
 		bool _hasNewFrame;
 
 		uint16 _numFrames;
@@ -233,8 +233,8 @@ private:
 		uint8   *_screenEffectsData;
 		uint32   _screenEffectsDataSize;
 
-		void VPTRWriteBlock(uint16 *frame, unsigned int dstBlock, unsigned int srcBlock, int count, bool alpha = false);
-		bool decodeFrame(uint16 *frame);
+		void VPTRWriteBlock(Graphics::Surface *surface, unsigned int dstBlock, unsigned int srcBlock, int count, bool alpha = false);
+		bool decodeFrame(Graphics::Surface *surface);
 	};
 
 	class VQAAudioTrack {
diff --git a/engines/bladerunner/vqa_player.cpp b/engines/bladerunner/vqa_player.cpp
index 1616459..7e41913 100644
--- a/engines/bladerunner/vqa_player.cpp
+++ b/engines/bladerunner/vqa_player.cpp
@@ -23,6 +23,7 @@
 #include "bladerunner/vqa_player.h"
 
 #include "bladerunner/bladerunner.h"
+#include "bladerunner/game_constants.h"
 
 #include "audio/decoders/raw.h"
 
@@ -71,7 +72,7 @@ void VQAPlayer::close() {
 	_s = nullptr;
 }
 
-int VQAPlayer::update(bool forceDraw) {
+int VQAPlayer::update(bool forceDraw, bool advanceFrame, Graphics::Surface *customSurface) {
 	uint32 now = 60 * _vm->_system->getMillis();
 	int result = -1;
 
@@ -104,14 +105,14 @@ int VQAPlayer::update(bool forceDraw) {
 		}
 
 		result = -1;
-	} else 	if (_frameNext > _frameEnd) {
+	} else if (_frameNext > _frameEnd) {
 		result = -3;
 	} else if (now < _frameNextTime) {
 		result = -1;
-	} else {
+	} else if (advanceFrame) {
 		_frame = _frameNext;
 		_decoder.readFrame(_frameNext, kVQAReadVideo);
-		_decoder.decodeVideoFrame(_frameNext);
+		_decoder.decodeVideoFrame(customSurface != nullptr ? customSurface : _surface, _frameNext);
 
 		int audioPreloadFrames = 14;
 
@@ -140,8 +141,9 @@ int VQAPlayer::update(bool forceDraw) {
 		_frameNext++;
 		result = _frame;
 	}
+
 	if (result < 0 && forceDraw && _frame != -1) {
-		_decoder.decodeVideoFrame(_frame, true);
+		_decoder.decodeVideoFrame(customSurface != nullptr ? customSurface : _surface, _frame, true);
 		result = _frame;
 	}
 	return result;
@@ -240,6 +242,10 @@ int VQAPlayer::getLoopEndFrame(int loop) {
 	return end;
 }
 
+int VQAPlayer::getFrameCount() {
+	return _decoder.numFrames();
+}
+
 void VQAPlayer::queueAudioFrame(Audio::AudioStream *audioStream) {
 	int n = _audioStream->numQueuedStreams();
 	if (n == 0)
diff --git a/engines/bladerunner/vqa_player.h b/engines/bladerunner/vqa_player.h
index d276b91..d95f8b3 100644
--- a/engines/bladerunner/vqa_player.h
+++ b/engines/bladerunner/vqa_player.h
@@ -50,6 +50,7 @@ class VQAPlayer {
 	Common::SeekableReadStream  *_s;
 	VQADecoder                   _decoder;
 	Audio::QueuingAudioStream   *_audioStream;
+	Graphics::Surface           *_surface;
 
 	int _frame;
 	int _frameNext;
@@ -77,7 +78,8 @@ public:
 	VQAPlayer(BladeRunnerEngine *vm, Graphics::Surface *surface)
 		: _vm(vm),
 		  _s(nullptr),
-		  _decoder(surface),
+		  _surface(surface),
+		  _decoder(),
 		  _audioStream(nullptr),
 		  _frame(-1),
 		  _frameNext(-1),
@@ -102,7 +104,7 @@ public:
 	bool open(const Common::String &name);
 	void close();
 
-	int  update(bool forceDraw = false);
+	int  update(bool forceDraw = false, bool advanceFrame = true, Graphics::Surface *customSurface = nullptr);
 	void updateZBuffer(ZBuffer *zbuffer);
 	void updateView(View *view);
 	void updateScreenEffects(ScreenEffects *screenEffects);
@@ -116,6 +118,8 @@ public:
 	int getLoopBeginFrame(int loop);
 	int getLoopEndFrame(int loop);
 
+	int getFrameCount();
+
 private:
 	void queueAudioFrame(Audio::AudioStream *audioStream);
 };
diff --git a/engines/bladerunner/waypoints.h b/engines/bladerunner/waypoints.h
index 5a2a1fc..183c089 100644
--- a/engines/bladerunner/waypoints.h
+++ b/engines/bladerunner/waypoints.h
@@ -31,9 +31,7 @@
 namespace BladeRunner {
 
 class Waypoints {
-#if BLADERUNNER_DEBUG_RENDERING
 	friend class BladeRunnerEngine;
-#endif
 
 	struct Waypoint {
 		int     setId;





More information about the Scummvm-git-logs mailing list