[Scummvm-git-logs] scummvm master -> 058592794f9c199316f26529558a530c957907d6

sev- sev at scummvm.org
Sat Oct 24 17:51:34 UTC 2020


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:
058592794f HADESCH: Add new engine for Hades' Challenge


Commit: 058592794f9c199316f26529558a530c957907d6
    https://github.com/scummvm/scummvm/commit/058592794f9c199316f26529558a530c957907d6
Author: Vladimir Serbinenko (phcoder at google.com)
Date: 2020-10-24T19:51:30+02:00

Commit Message:
HADESCH: Add new engine for Hades' Challenge

The game is completable but 3 arcade sequences at the end of Minotaur, Medusa and Troy quests.
Probably full of bugs but I already publish it for consideration

Changed paths:
  A engines/hadesch/ambient.cpp
  A engines/hadesch/ambient.h
  A engines/hadesch/baptr.cpp
  A engines/hadesch/baptr.h
  A engines/hadesch/configure.engine
  A engines/hadesch/detection.cpp
  A engines/hadesch/detection_tables.h
  A engines/hadesch/enums.h
  A engines/hadesch/event.h
  A engines/hadesch/gfx_context.cpp
  A engines/hadesch/gfx_context.h
  A engines/hadesch/hadesch.cpp
  A engines/hadesch/hadesch.h
  A engines/hadesch/herobelt.cpp
  A engines/hadesch/herobelt.h
  A engines/hadesch/hotzone.cpp
  A engines/hadesch/hotzone.h
  A engines/hadesch/metaengine.cpp
  A engines/hadesch/module.mk
  A engines/hadesch/persistent.cpp
  A engines/hadesch/persistent.h
  A engines/hadesch/pod_file.cpp
  A engines/hadesch/pod_file.h
  A engines/hadesch/pod_image.cpp
  A engines/hadesch/pod_image.h
  A engines/hadesch/rooms/argo.cpp
  A engines/hadesch/rooms/athena.cpp
  A engines/hadesch/rooms/catacombs.cpp
  A engines/hadesch/rooms/credits.cpp
  A engines/hadesch/rooms/crete.cpp
  A engines/hadesch/rooms/daedalus.cpp
  A engines/hadesch/rooms/ferry.cpp
  A engines/hadesch/rooms/hadesthrone.cpp
  A engines/hadesch/rooms/intro.cpp
  A engines/hadesch/rooms/medisle.cpp
  A engines/hadesch/rooms/medusa.cpp
  A engines/hadesch/rooms/minos.cpp
  A engines/hadesch/rooms/minotaur.cpp
  A engines/hadesch/rooms/monster.cpp
  A engines/hadesch/rooms/monster.h
  A engines/hadesch/rooms/monster/cyclops.cpp
  A engines/hadesch/rooms/monster/illusion.cpp
  A engines/hadesch/rooms/monster/projectile.cpp
  A engines/hadesch/rooms/monster/typhoon.cpp
  A engines/hadesch/rooms/olympus.cpp
  A engines/hadesch/rooms/options.cpp
  A engines/hadesch/rooms/priam.cpp
  A engines/hadesch/rooms/quiz.cpp
  A engines/hadesch/rooms/riverstyx.cpp
  A engines/hadesch/rooms/seriphos.cpp
  A engines/hadesch/rooms/trojan.cpp
  A engines/hadesch/rooms/troy.cpp
  A engines/hadesch/rooms/volcano.cpp
  A engines/hadesch/rooms/walloffame.cpp
  A engines/hadesch/table.cpp
  A engines/hadesch/table.h
  A engines/hadesch/tag_file.cpp
  A engines/hadesch/tag_file.h
  A engines/hadesch/video.cpp
  A engines/hadesch/video.h
    AUTHORS
    NEWS.md
    devtools/credits.pl
    gui/credits.h


diff --git a/AUTHORS b/AUTHORS
index d37ba83668..8af2ee51fc 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -182,6 +182,9 @@ ScummVM Team
        Scott Thomas
        Jordi Vilalta Prat
 
+    Hades' Challenge:
+       Vladimir Serbinenko/Google
+
     HDB:
        Eugene Sandulenko
        Nipun Garg                     - GSoC student
diff --git a/NEWS.md b/NEWS.md
index 69b2fd0e7e..6fce3f671d 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -8,6 +8,7 @@ For a more comprehensive changelog of the latest experimental code, see:
    - Added support for Escape from Monkey Island.
    - Added support for The Longest Journey.
    - Added support for Myst 3: Exile.
+   - Added support for Hades' Challenge.
 
  General:
    - Switched ScummVM GUI output to UTF-32.
diff --git a/devtools/credits.pl b/devtools/credits.pl
index 33eea17332..01b084dc26 100755
--- a/devtools/credits.pl
+++ b/devtools/credits.pl
@@ -1540,8 +1540,15 @@ begin_credits("Credits");
 				add_person("", "Faalagorn", "Few code improvements");
 				add_person("", "orangeforest11", "Few engine improvements");
 			end_persons();
+                end_section();
+
+		begin_section("Hades' Challenge", "hadesch_contrib");
+			begin_persons();
+				add_person("Vladimir Serbinenko/Google", "phcoder", "Engine implementation");
+			end_persons();
 		end_section();
 
+
 		add_paragraph("And to all the contributors, users, and beta testers we've missed. Thanks!");
 
 	end_section();
diff --git a/engines/hadesch/ambient.cpp b/engines/hadesch/ambient.cpp
new file mode 100644
index 0000000000..74be4c7d78
--- /dev/null
+++ b/engines/hadesch/ambient.cpp
@@ -0,0 +1,357 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+#include "hadesch/ambient.h"
+
+namespace Hadesch {
+class AmbientAnimStarter : public EventHandler {
+public:
+	void operator()() override {
+		_anim.play(true);
+	}
+
+	AmbientAnimStarter(AmbientAnim anim) {
+		_anim = anim;
+	}
+private:
+	AmbientAnim _anim;
+};
+
+class AmbientAnimPlayEnded : public EventHandler {
+public:
+	void operator()() override {
+		_anim.playFinished(_reschedule);
+	}
+
+	AmbientAnimPlayEnded(AmbientAnim anim, bool reschedule) {
+		_anim = anim;
+		_reschedule = reschedule;
+	}
+private:
+	AmbientAnim _anim;
+	bool _reschedule;
+};
+
+AmbientAnim::AmbientAnim(const Common::String &animName,
+			 const Common::String &sound, int zValue,
+			 int minint, int maxint, AnimType loop,
+			 Common::Point offset, PanType pan) {
+	_internal = Common::SharedPtr<AmbiantAnimInternal>(
+		new AmbiantAnimInternal());
+	_internal->_descs.push_back(AmbientDesc(animName, sound));
+	_internal->_minInterval = minint;
+	_internal->_maxInterval = maxint;
+	_internal->_offset = offset;
+	_internal->_loopType = loop;
+	_internal->_zValue = zValue;
+	_internal->_paused = false;
+	_internal->_playing = false;
+	_internal->_pan = pan;
+	_internal->_isFwd = true;
+}
+
+AmbientAnim::AmbientAnim(const Common::Array<AmbientDesc> &descs, int zValue,
+			 int minint, int maxint, AnimType loop,
+			 Common::Point offset, PanType pan) {
+	_internal = Common::SharedPtr<AmbiantAnimInternal>(
+		new AmbiantAnimInternal());
+	_internal->_descs = descs;
+	_internal->_minInterval = minint;
+	_internal->_maxInterval = maxint;
+	_internal->_offset = offset;
+	_internal->_loopType = loop;
+	_internal->_zValue = zValue;
+	_internal->_paused = false;
+	_internal->_playing = false;
+	_internal->_pan = pan;
+	_internal->_isFwd = true;
+}
+
+AmbientAnim::AmbientAnim() {
+	_internal = Common::SharedPtr<AmbiantAnimInternal>(
+		new AmbiantAnimInternal());
+	_internal->_minInterval = 0;
+	_internal->_maxInterval = 0;
+	_internal->_loopType = KEEP_LOOP;
+	_internal->_zValue = 0;
+	_internal->_paused = false;
+	_internal->_playing = false;
+	_internal->_isFwd = true;
+}
+
+void AmbientAnim::pause() {
+	_internal->_paused = true;
+}
+
+void AmbientAnim::unpause() {
+	_internal->_paused = false;
+}
+
+void AmbientAnim::hide() {
+	pause();
+	Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+	room->stopAnim(_internal->_descs[0]._animName);
+	_internal->_playing = false;
+	_internal->_paused = true;
+}
+
+void AmbientAnim::selectFirstFrame() {
+	Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+	room->selectFrame(_internal->_descs[0]._animName, _internal->_zValue,
+			  0, _internal->_offset);
+}
+
+void AmbientAnim::unpauseAndFirstFrame() {
+	unpause();
+	selectFirstFrame();
+}
+
+bool AmbientAnim::isPanOK() {
+	Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+
+	if (_internal->_pan == PAN_ANY)
+		return true;
+
+	if (_internal->_pan == PAN_LEFT && room->isPanLeft())
+		return true;
+
+	if (_internal->_pan == PAN_RIGHT && room->isPanRight())
+		return true;
+
+	return false;
+}
+
+bool AmbientAnim::isReady() {
+ 	return !_internal->_paused && !_internal->_playing && isPanOK();
+}
+
+void AmbientAnim::play(bool reschedule) {
+	Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+	if (_internal->_paused || _internal->_playing || !isPanOK()) {
+		if (reschedule)
+			schedule();
+		return;
+	}
+	_internal->_playing = true;
+	unsigned variant = 0;
+	if (_internal->_descs.size() > 1) {
+		variant = g_vm->getRnd().getRandomNumberRng(0, _internal->_descs.size() - 1);
+		for (unsigned i = 0; i < _internal->_descs.size(); i++) {
+			if (i != variant)
+				room->stopAnim(_internal->_descs[i]._animName);
+		}
+	}
+
+	PlayAnimParams params = PlayAnimParams::disappear();
+
+	switch (_internal->_loopType) {
+	case DISAPPEAR:
+		params = PlayAnimParams::disappear();
+		break;
+	case KEEP_LOOP:
+		params = PlayAnimParams::keepLastFrame();
+		break;
+	case BACK_AND_FORTH:
+		if (_internal->_isFwd) {
+			params = PlayAnimParams::keepLastFrame();
+		} else {
+			params = PlayAnimParams::disappear().backwards();
+		}
+		_internal->_isFwd = !_internal->_isFwd;
+		break;
+	}
+
+	room->playAnim(_internal->_descs[variant]._animName, _internal->_zValue,
+		       params,
+		       Common::SharedPtr<EventHandler>(new AmbientAnimPlayEnded(*this, reschedule)),
+		       _internal->_offset);		
+	
+	if (_internal->_descs[variant]._soundName != "")
+		room->playSound(_internal->_descs[variant]._soundName, -1);
+}
+	
+void AmbientAnim::schedule() {
+	if (_internal->_minInterval >= 0 && _internal->_maxInterval >= 0)
+		g_vm->addTimer(
+			Common::SharedPtr<EventHandler>(new AmbientAnimStarter(*this)),
+			g_vm->getRnd().getRandomNumberRng(_internal->_minInterval,
+							  _internal->_maxInterval));
+}
+
+void AmbientAnim::playFinished(bool reschedule) {
+	_internal->_playing = false;
+	if (reschedule)
+		schedule();
+}
+
+void AmbientAnim::start() {
+	if (_internal->_loopType == KEEP_LOOP) {
+		selectFirstFrame();
+	}
+	schedule();
+}
+
+void AmbientAnimWeightedSet::readTableFilePriam(const TextTable &table) {
+	for (int row = 0; row < table.size(); row++) {
+		AmbientAnimWeightedSetElement el;
+		el.name = table.get(row, "name");
+		el.weight = table.get(row, "weight").asUint64();
+		el.valid = table.get(row, "anim") != "";
+		if (el.valid)
+			el.anim = AmbientAnim(table.get(row, "anim"),
+					      table.get(row, "sound"),
+					      table.get(row, "depth").asUint64(),
+					      -1, -1, AmbientAnim::KEEP_LOOP, Common::Point(0, 0),
+					      AmbientAnim::PAN_ANY);
+		// TODO: volume
+		_elements.push_back(el);
+	}
+}
+
+void AmbientAnimWeightedSet::readTableFile(const TextTable &table, AmbientAnim::PanType pan) {
+	for (int row = 0; row < table.size(); row++) {
+		AmbientAnimWeightedSetElement el;
+		el.name = table.get(row, "anim");
+		el.weight = 1;
+		el.valid = table.get(row, "anim") != "";
+		if (el.valid)
+			el.anim = AmbientAnim(table.get(row, "anim"),
+					      table.get(row, "sound"),
+					      table.get(row, "Z").asUint64(),
+					      -1, -1, AmbientAnim::KEEP_LOOP, Common::Point(
+						      table.get(row, "X").asUint64(),
+						      table.get(row, "Y").asUint64()),
+					      pan);
+		// TODO: volume
+		_elements.push_back(el);
+	}
+}
+
+void AmbientAnimWeightedSet::firstFrame() {
+	for (unsigned i = 0; i < _elements.size(); i++) {
+		if (_elements[i].valid)
+			_elements[i].anim.selectFirstFrame();
+	}
+}
+
+void AmbientAnimWeightedSet::tick() {
+	int chosen = -1, chosenWeight = -1;
+	// This is not how weighted random should be
+	// but this is howoriginal game generates it and
+	// it's probably slightly wrong from mathematical
+	// point of view
+	for (unsigned i = 0; i < _elements.size(); i++) {
+		if (!_elements[i].anim.isReady())
+			continue;
+		int curWeight = _elements[i].weight * g_vm->getRnd().getRandomNumberRng(
+			0, 100);
+		if (curWeight > chosenWeight) {
+			chosen = i;
+			chosenWeight = curWeight;
+		}
+	}
+	if (chosen < 0 || !_elements[chosen].valid)
+		return;
+	_elements[chosen].anim.play(false);
+}
+
+void AmbientAnimWeightedSet::pause(const Common::String &name) {
+	for (unsigned i = 0; i < _elements.size(); i++) {
+		if (_elements[i].name == name && _elements[i].valid) {
+			_elements[i].anim.pause();
+		}
+	}
+}
+
+void AmbientAnimWeightedSet::unpause(const Common::String &name) {
+	for (unsigned i = 0; i < _elements.size(); i++) {
+		if (_elements[i].name == name && _elements[i].valid) {
+			_elements[i].anim.unpause();
+		}
+	}
+}
+
+void AmbientAnimWeightedSet::play(const Common::String &name, bool reschedule) {
+	for (unsigned i = 0; i < _elements.size(); i++) {
+		if (_elements[i].name == name && _elements[i].valid) {
+			_elements[i].anim.play(reschedule);
+		}
+	}
+}
+
+void AmbientAnimWeightedSet::unpauseAndFirstFrame(const Common::String &name) {
+	for (unsigned i = 0; i < _elements.size(); i++) {
+		if (_elements[i].name == name && _elements[i].valid) {
+			_elements[i].anim.unpauseAndFirstFrame();
+		}
+	}
+}
+
+void AmbientAnimWeightedSet::hide(const Common::String &name) {
+	for (unsigned i = 0; i < _elements.size(); i++) {
+		if (_elements[i].name == name && _elements[i].valid) {
+			_elements[i].anim.hide();
+		}
+	}
+}
+
+void AnimClickables::playChosen(const Common::String &name, int counter, const EventHandlerWrapper &event) {
+	Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+	int rk = _table.rowCount(name);
+	if (rk == 0) {
+		event();
+		return;
+	}
+	counter %= rk;
+	Common::String smacker = _table.get(name, "smacker", counter);
+	Common::String anim = _table.get(name, "anim", counter);
+	Common::String sound = _table.get(name, "sound", counter);
+	int zValue = _table.get(name, "Z", counter).asUint64();
+	if (smacker != "")
+		room->playVideo(smacker.substr(1), zValue, event,
+				Common::Point(_table.get(name, "smackerX", counter).asUint64(),
+					      _table.get(name, "smackerY", counter).asUint64()));
+	else if (anim != "")
+		room->playAnimWithSound(
+			anim, sound, zValue, PlayAnimParams::disappear(), event,
+			Common::Point(_table.get(name, "X", counter).asUint64(),
+				      _table.get(name, "Y", counter).asUint64()));
+	else if (sound != "")
+		room->playSound(sound, event);
+	else
+		event();
+}
+
+// TODO: should counters be persistent?
+void AnimClickables::playNext(const Common::String &name, const EventHandlerWrapper &event) {
+	playChosen(name, _counters[name], event);
+	_counters[name]++;
+}
+
+void AnimClickables::readTable(Common::SharedPtr<Hadesch::VideoRoom> room, const Common::String &name) {
+	_table = TextTable(Common::SharedPtr<Common::SeekableReadStream>(room->openFile(name)), 14);
+}
+
+}
diff --git a/engines/hadesch/ambient.h b/engines/hadesch/ambient.h
new file mode 100644
index 0000000000..0c71f28ffe
--- /dev/null
+++ b/engines/hadesch/ambient.h
@@ -0,0 +1,131 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "common/str.h"
+#include "common/rect.h"
+#include "common/noncopyable.h"
+
+#ifndef HADESCH_AMBIENT_H
+#define HADESCH_AMBIENT_H
+
+namespace Hadesch {
+class AmbientAnim {
+public:
+	struct AmbientDesc {
+		Common::String _animName;
+		Common::String _soundName;
+		AmbientDesc(Common::String animName, Common::String soundName) {
+			_animName = animName;
+			_soundName = soundName;
+		}
+	};
+
+	enum PanType {
+		PAN_ANY,
+		PAN_LEFT,
+		PAN_RIGHT
+	};
+
+  	enum AnimType {
+		DISAPPEAR,
+		KEEP_LOOP,
+		BACK_AND_FORTH
+	};
+
+	AmbientAnim(const Common::String &animName,
+		    const Common::String &sound, int zValue,
+		    int minint, int maxint, AnimType loop,
+		    Common::Point offset, PanType pan);
+	AmbientAnim(const Common::Array<AmbientDesc> &descs, int zValue,
+		    int minint, int maxint, AnimType loop,
+		    Common::Point offset, PanType pan);
+	AmbientAnim();
+	void play(bool reschedule);
+	void schedule();
+	void start();
+	void pause();
+	void unpause();
+	void hide();
+  	void unpauseAndFirstFrame();
+    	void selectFirstFrame();
+	void playFinished(bool reschedule);
+	bool isReady();
+private:
+	class AmbiantAnimInternal : Common::NonCopyable {
+	public:
+		Common::Array<AmbientDesc> _descs;
+		int _minInterval, _maxInterval;
+		int _zValue;
+		AnimType _loopType;
+		bool _isFwd;
+		Common::Point _offset;
+		bool _playing;
+		bool _paused;
+		PanType _pan;
+	};
+
+	bool isPanOK();
+
+	Common::SharedPtr<AmbiantAnimInternal> _internal;
+};
+
+	
+class AmbientAnimWeightedSet {
+public:
+	void readTableFilePriam(const TextTable &table);
+	void readTableFile(const TextTable &table, AmbientAnim::PanType pan);
+	void tick();
+	void firstFrame();
+	void pause(const Common::String &name);
+	void unpause(const Common::String &name);
+	void unpauseAndFirstFrame(const Common::String &name);
+	void hide(const Common::String &name);
+	void play(const Common::String &name, bool reschedule);
+private:
+	struct AmbientAnimWeightedSetElement {
+		AmbientAnim anim;
+		int weight;
+		bool valid;
+		Common::String name;
+	};
+	Common::Array<AmbientAnimWeightedSetElement> _elements;
+};
+
+class AnimClickables {
+public:
+	void playNext(const Common::String &name, const EventHandlerWrapper &event);
+	void playChosen(const Common::String &name, int counter, const EventHandlerWrapper &event);
+	void setTable(const TextTable table) {
+		_table = table;
+	}
+	void readTable(Common::SharedPtr<Hadesch::VideoRoom> room,
+		       const Common::String &name);
+
+private:
+	TextTable _table;
+	Common::HashMap<Common::String, int> _counters;
+};
+
+
+}
+#endif
diff --git a/engines/hadesch/baptr.cpp b/engines/hadesch/baptr.cpp
new file mode 100644
index 0000000000..5bc1e37f7b
--- /dev/null
+++ b/engines/hadesch/baptr.cpp
@@ -0,0 +1,35 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/baptr.h"
+
+namespace Hadesch {
+class ByteArrayDeleter {
+public:
+	void operator()(byte *ptr) { delete [] ptr; }
+};
+
+Common::SharedPtr<byte> sharedPtrByteAlloc(size_t sz) {
+	return Common::SharedPtr<byte>(new (std::nothrow) byte[sz], ByteArrayDeleter());
+}
+}
diff --git a/engines/hadesch/baptr.h b/engines/hadesch/baptr.h
new file mode 100644
index 0000000000..9d752c8fea
--- /dev/null
+++ b/engines/hadesch/baptr.h
@@ -0,0 +1,32 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#ifndef HADESCH_BAPTR_H
+#define HADESCH_BAPTR_H
+
+#include "common/ptr.h"
+
+namespace Hadesch {
+Common::SharedPtr<byte> sharedPtrByteAlloc(size_t sz);
+}
+#endif
diff --git a/engines/hadesch/configure.engine b/engines/hadesch/configure.engine
new file mode 100644
index 0000000000..dad132c7e4
--- /dev/null
+++ b/engines/hadesch/configure.engine
@@ -0,0 +1,3 @@
+# This file is included from the main "configure" script
+# add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps]
+add_engine hadesch "Hades Challenge" no "" "" "highres"
diff --git a/engines/hadesch/detection.cpp b/engines/hadesch/detection.cpp
new file mode 100644
index 0000000000..9cd399393b
--- /dev/null
+++ b/engines/hadesch/detection.cpp
@@ -0,0 +1,83 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "common/system.h"
+#include "common/savefile.h"
+
+#include "engines/advancedDetector.h"
+
+#include "hadesch/hadesch.h"
+
+#include "detection_tables.h"
+
+namespace Hadesch {
+static const PlainGameDescriptor hadeschGames[] = {
+	{"hadesch", "Hades Challenge"},
+	{0, 0}
+};
+
+// The list is pretty long but it's because we need just a few files but
+// in pretty deep paths:
+// * Setup.exe [Russian-Windows]
+// * WIN9x/WORLD/wd.pod [English-Windows]
+// * CDAssets/OLYMPUS/ol.pod [English-Windows]
+// * Scenes/OLYMPUS/ol.pod [English-Mac and Russian-Windows]
+// * Hades - Copy To Hard Drive/Hades Challenge/World/wd.pod [English-Mac]
+// * Hades - Copy To Hard Drive/Hades Challenge/World/wd.pod [English-Mac]
+// The difference between 2 last one is how the files were copied
+static const char *const directoryGlobs[] = {
+	"WIN9x",
+	"WORLD",
+	"CDAssets",
+	"OLYMPUS",
+	"Scenes",
+	"Hades_-_Copy_To_Hard_Drive",
+	"Hades - Copy To Hard Drive",
+	"Hades Challenge",
+	"Hades_Challenge",
+	0
+};
+}
+
+class HadeschMetaEngineDetection : public AdvancedMetaEngineDetection {
+public:
+	HadeschMetaEngineDetection() : AdvancedMetaEngineDetection(Hadesch::gameDescriptions, sizeof(ADGameDescription), Hadesch::hadeschGames) {
+		// mac puts wd.pod in Hades - Copy To Hard Drive/Hades Challenge/World. So we need 4 levels
+		_maxScanDepth = 4;
+		_directoryGlobs = Hadesch::directoryGlobs;
+	}
+
+	const char *getEngineId() const override {
+		return "hadesch";
+	}
+
+	const char *getName() const override {
+		return "Hades Challenge";
+	}
+
+	const char *getOriginalCopyright() const override {
+		return "Hades Challenge (C) Disney's Interactive";
+	}
+};
+
+REGISTER_PLUGIN_STATIC(HADESCH_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, HadeschMetaEngineDetection);
diff --git a/engines/hadesch/detection_tables.h b/engines/hadesch/detection_tables.h
new file mode 100644
index 0000000000..200497f62b
--- /dev/null
+++ b/engines/hadesch/detection_tables.h
@@ -0,0 +1,98 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+
+#ifndef HADESCH_DETECTION_TABLES_H
+#define HADESCH_DETECTION_TABLES_H
+
+namespace Hadesch {
+
+static const ADGameDescription gameDescriptions[] = {
+
+	// Hades Challenge
+	{
+		"hadesch",
+		0,
+		{
+			{"hadesch.exe", 0, "178b3a69171cb5a4eeeddd0d5993b8c5", 1134592},
+			{"WD.POD", 0, "be7030fc4229e69e719ee2c756eb6ba1", 7479768},
+			{"ol.pod", 0, "7cabba8d1d4f1239e312e045ef4e9735", 5621074},
+			AD_LISTEND
+		},
+		Common::EN_ANY,
+		Common::kPlatformWindows,
+		ADGF_UNSTABLE | ADGF_DROPPLATFORM,
+		GUIO1(GUIO_NOMIDI)
+
+	},
+	{
+		"hadesch",
+		0,
+		{
+			{"Hades Challenge PPC", 0, "c7213a365a3cab7e9f2b423fa4a204f5", 1724646},
+			{"WD.POD", 0, "be7030fc4229e69e719ee2c756eb6ba1", 7479768},
+			{"ol.pod", 0, "7cabba8d1d4f1239e312e045ef4e9735", 5621074},
+			AD_LISTEND
+		},
+		Common::EN_ANY,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE | ADGF_DROPPLATFORM | ADGF_MACRESFORK,
+		GUIO1(GUIO_NOMIDI)
+
+	},
+	{
+		"hadesch",
+		0,
+		{
+			{"hadesch.exe", 0, "660735787346ab1bfe0d219bea441486", 1007616},
+			{"WD.POD", 0, "5098edae755135814bb86f2676c41cc2", 8691909},
+			{"ol.pod", 0, "c82e105d9013edc2cc20f0a630e304d5", 5684953},
+			AD_LISTEND
+		},
+		Common::RU_RUS,
+		Common::kPlatformWindows,
+		ADGF_UNSTABLE | ADGF_DROPPLATFORM,
+		GUIO1(GUIO_NOMIDI)
+
+	},
+	{
+		"hadesch",
+		0,
+		{
+			{"setup.exe", 0, "853c199f1ef35d576213f71092bcd0c3", 7491209},
+			{"ol.pod", 0, "c82e105d9013edc2cc20f0a630e304d5", 5684953},
+			AD_LISTEND
+		},
+		Common::RU_RUS,
+		Common::kPlatformWindows,
+		ADGF_UNSTABLE | ADGF_DROPPLATFORM,
+		GUIO1(GUIO_NOMIDI)
+
+	},
+
+	AD_TABLE_END_MARKER
+};
+
+} // End of namespace Hadesch
+
+#endif
diff --git a/engines/hadesch/enums.h b/engines/hadesch/enums.h
new file mode 100644
index 0000000000..c6c47b306e
--- /dev/null
+++ b/engines/hadesch/enums.h
@@ -0,0 +1,177 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#ifndef HADESCH_ENUMS_H
+#define HADESCH_ENUMS_H
+
+namespace Hadesch {
+enum {
+	kHadeschDebugGeneral = 1 << 0,
+	kHadeschDebugResources = 1 << 1,
+	kHadeschDebugMessagingSystem = 1 << 2,
+	kHadeschDebugDialogs = 1 << 3
+};
+
+enum Gender {
+	kFemale = 0,
+	kMale = 1,
+	// Make it 2, rather than -1, so that we can serialize it in one 1
+	// byte.
+	kUnknown = 2
+};
+
+enum Quest {
+	    kNoQuest,
+	    kCreteQuest,
+	    kTroyQuest,
+	    kMedusaQuest,
+	    kRescuePhilQuest,
+	    kEndGame,
+	    kNumQuests
+};
+
+enum RoomId {
+	kInvalidRoom = 0,
+	kIntroRoom = 1,
+	kOlympusRoom = 2,
+	kWallOfFameRoom = 3,
+	kSeriphosRoom = 4,
+	kAthenaRoom = 5,
+	kMedIsleRoom = 6,
+	kMedusaPuzzle = 7,
+	kArgoRoom = 8,
+	kTroyRoom = 9,
+	kCatacombsRoom = 10,
+	kPriamRoom = 11,
+	kTrojanHorsePuzzle = 12,
+	kCreteRoom = 13,
+	kMinosPalaceRoom = 14,
+	kDaedalusRoom = 15,
+	kMinotaurPuzzle = 16,
+	kVolcanoRoom = 17,
+	kRiverStyxRoom = 18,
+	kHadesThroneRoom = 19,
+	kFerrymanPuzzle = 20,
+	kMonsterPuzzle = 21,
+	kQuiz = 22,
+	kCreditsRoom = 23,
+	kOptionsRoom = 24,
+	kNumRooms
+};
+
+enum StatueId {
+	kBacchusStatue = 0,
+	kHermesStatue = 1,
+	kZeusStatue = 2,
+	kPoseidonStatue = 3,
+	kAresStatue = 4,
+	kAphroditeStatue = 5,
+	kApolloStatue = 6,
+	kArtemisStatue = 7,
+	kDemeterStatue = 8,
+	kAthenaStatue = 9,
+	kHeraStatue = 10,
+	kHephaestusStatue = 11,
+	kNumStatues
+};
+
+enum InventoryItem {
+	kNone = 0,
+	kStraw = 2,
+	kStone = 3,
+	kBricks = 4,
+	kMessage = 5,
+	kKey = 6,
+	kDecree = 7,
+	kWood = 8,
+	kHornlessStatue1 = 9,
+	kHornlessStatue2 = 10,
+	kHornlessStatue3 = 11,
+	kHornlessStatue4 = 12,
+	kHornedStatue = 13,
+	kCoin = 14,
+	kPotion = 15,
+	kShield = 16,
+	kSword = 17,
+	kBag = 18,
+	kHelmet = 19,
+	kSandals = 20,
+	kTorch = 21
+};
+
+// Also includes InventoryItem - 1
+enum HeroBeltFrame {
+	kLightning1 = 21,
+	kLightning2 = 22,
+	kLightning3 = 23,
+	kNumberI = 24,
+	kNumberII = 25,
+	kNumberIII = 26,
+	kQuestScroll = 27,
+	kQuestScrollHighlighted = 28,
+	kHadesScroll = 29,
+	kHadesScrollHighlighted = 30,
+	kOptionsButton = 31,
+	kInactiveHints = 32,
+	kActiveHints = 33,
+	kBranchOfLife = 34,
+	kReturnToWall = 35,
+	kPowerOfWisdom = 38,
+	kPowerOfStrength = 39,
+	kPowerOfStealth = 40
+};
+
+enum FateId {
+	kLachesis,
+	kAtropos,
+	kClotho,
+	kNumFates
+};
+
+enum CatacombsPosition {
+	kCatacombsLeft = 0,
+	kCatacombsCenter = 1,
+	kCatacombsRight = 2
+};
+
+enum CatacombsPath {
+	kCatacombsHelen = 0,
+	kCatacombsGuards = 1,
+	kCatacombsPainAndPanic = 2
+};
+
+enum CatacombsLevel {
+	kCatacombLevelSign,
+	kCatacombLevelTorch,
+	kCatacombLevelMusic
+};
+
+enum HeroPower {
+	kPowerNone = -1,
+	kPowerStrength = 0,
+	kPowerStealth = 1,
+	kPowerWisdom = 2
+};
+}
+
+#endif
diff --git a/engines/hadesch/event.h b/engines/hadesch/event.h
new file mode 100644
index 0000000000..11faadbb21
--- /dev/null
+++ b/engines/hadesch/event.h
@@ -0,0 +1,64 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#ifndef HADESCH_EVENT_H
+#define HADESCH_EVENT_H
+
+namespace Hadesch {
+
+class EventHandler {
+public:
+	virtual void operator()() = 0;
+	virtual ~EventHandler() {}
+};
+
+class EventHandlerWrapper {
+public:
+	void operator()() const;
+	bool operator==(int eventId) const;
+
+	EventHandlerWrapper(int eventId) {
+		_eventId = eventId;
+	}
+
+	EventHandlerWrapper() {
+		_eventId = -1;
+	}
+
+	EventHandlerWrapper(Common::SharedPtr<EventHandler> handler) {
+		_handler = handler;
+		_eventId = -1;
+	}
+
+	Common::String getDebugString() const {
+		return Common::String::format("eventid=%d, handler is %s", _eventId,
+					      !!_handler ? "valid" : "null");
+	}
+
+private:
+	Common::SharedPtr<EventHandler> _handler;
+	int _eventId;
+};
+}
+
+#endif
diff --git a/engines/hadesch/gfx_context.cpp b/engines/hadesch/gfx_context.cpp
new file mode 100644
index 0000000000..c4c467e390
--- /dev/null
+++ b/engines/hadesch/gfx_context.cpp
@@ -0,0 +1,107 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/gfx_context.h"
+#include "hadesch/video.h"
+#include "hadesch/baptr.h"
+#include "common/system.h"
+#include "graphics/palette.h"
+
+namespace Hadesch {
+
+void blendVideo8To8(byte *targetPixels, int targetPitch, int targetW, int targetH,
+		    byte *sourcePixels, int sourceW, int sourceH, Common::Point offset) {
+	int ymin = MAX(0, -offset.y);
+	int ymax = MIN(sourceH, targetH - offset.y);
+	for (int y = ymin; y < ymax; y++) {
+		int xmin = MAX(0, -offset.x);
+		int xmax = MIN(sourceW, targetW - offset.x);
+		const byte *inptr = sourcePixels + sourceW * y + xmin;
+		byte *outptr = targetPixels + targetPitch * (y + offset.y) + offset.x + xmin;
+		for (int x = xmin; x < xmax; x++, inptr++, outptr++) {
+			if (*inptr)
+				*outptr = *inptr;
+		}
+	}
+}
+
+void GfxContext8Bit::blitPodImage(byte *sourcePixels, int sourcePitch, int sourceW, int sourceH,
+				  byte *sourcePalette, size_t ncolours, Common::Point offset) {
+	
+	blendVideo8To8(_pixels.get(), _pitch,
+		       _w, _h, sourcePixels, sourceW, sourceH,
+		       offset);
+	for (unsigned i = 0; i < ncolours; i++) {
+		int col = sourcePalette[4 * i] & 0xff;
+		
+		_palette[3 * col    ] = sourcePalette[4 * i + 1];
+		_palette[3 * col + 1] = sourcePalette[4 * i + 2];
+		_palette[3 * col + 2] = sourcePalette[4 * i + 3];
+		_paletteUsed[col] = true;
+	}
+}
+
+void GfxContext8Bit::blitVideo(byte *sourcePixels, int sourcePitch, int sourceW, int sourceH,
+			       byte *sourcePalette, Common::Point offset) {
+	blendVideo8To8(_pixels.get(), _pitch, _w, _h, sourcePixels, sourceW, sourceH, offset);
+	for (int i = 0; i < 256; i++)
+		if (!_paletteUsed[i]) {
+			_palette[3 * i] = sourcePalette[3 * i];
+			_palette[3 * i + 1] = sourcePalette[3 * i + 1];
+			_palette[3 * i + 2] = sourcePalette[3 * i + 2];
+		}
+}
+
+void GfxContext8Bit::fade(int val) {
+	if (val == 0x100)
+		return;
+	for (int i = 0; i < 256 * 3; i++) {
+		_palette[i] = ((_palette[i] & 0xff) * val) >> 8;
+	}
+}
+
+void GfxContext8Bit::clear() {
+	memset(_pixels.get(), 0, _w * _h);
+	memset(_palette, 0, sizeof(_palette));
+	memset(_paletteUsed, 0, sizeof(_paletteUsed));
+}
+
+GfxContext8Bit::GfxContext8Bit(int canvasW, int canvasH) {
+	_w = canvasW;
+	_h = canvasH;
+	_pitch = _w;
+	_pixels = sharedPtrByteAlloc(_w * _h);
+	memset(_palette, 0, sizeof(_palette));
+	memset(_paletteUsed, 0, sizeof(_paletteUsed));
+}
+
+void GfxContext8Bit::renderToScreen(Common::Point viewPoint) {
+	if (_palette) {
+		g_system->getPaletteManager()->setPalette(_palette, 0, 256);
+	}
+
+	g_system->copyRectToScreen(_pixels.get() + viewPoint.x + _pitch * viewPoint.y, _w, 0, 0,
+				   kVideoWidth, kVideoHeight);
+}
+
+}
diff --git a/engines/hadesch/gfx_context.h b/engines/hadesch/gfx_context.h
new file mode 100644
index 0000000000..94e4780c78
--- /dev/null
+++ b/engines/hadesch/gfx_context.h
@@ -0,0 +1,71 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#ifndef HADESCH_GFX_CONTEXT_H
+#define HADESCH_GFX_CONTEXT_H
+
+#include "common/ptr.h"
+#include "common/rect.h"
+
+namespace Hadesch {
+class GfxContext {
+public:
+	virtual void blitVideo(byte *sourcePixels, int sourcePitch, int sourceW, int sourceH,
+			       byte *palette, Common::Point offset) = 0;
+	// Use only in pod_image.cpp because of palette format
+	virtual void blitPodImage(byte *sourcePixels, int sourcePitch, int sourceW, int sourceH,
+				  byte *palette, size_t ncolours, Common::Point offset) = 0;
+	virtual void clear() = 0;
+	virtual void fade(int val) = 0;
+	virtual void renderToScreen(Common::Point viewPoint) = 0;
+	virtual ~GfxContext() {}
+};
+
+class GfxContext8Bit : public GfxContext {
+public:
+  	void blitVideo(byte *sourcePixels, int sourcePitch, int sourceW, int sourceH,
+		       byte *palette, Common::Point offset) override;
+	void blitPodImage(byte *sourcePixels, int sourcePitch, int sourceW, int sourceH,
+			  byte *palette, size_t ncolours, Common::Point offset) override;
+	void clear() override;
+	void fade(int val) override;
+	void renderToScreen(Common::Point viewPoint) override;
+
+	GfxContext8Bit(int canvasW, int canvasH);
+	~GfxContext8Bit() {}
+
+private:
+	Common::SharedPtr<byte> _pixels;
+	byte _palette[256 * 4];
+	bool _paletteUsed[256];
+	int _pitch;
+	int _w;
+	int _h;
+};
+
+void blendVideo8To8(byte *targetPixels, int targetPitch, int targetW, int targetH,
+		    byte *sourcePixels, int sourceW, int sourceH, Common::Point offset);
+
+}
+
+#endif
diff --git a/engines/hadesch/hadesch.cpp b/engines/hadesch/hadesch.cpp
new file mode 100644
index 0000000000..55bde4f072
--- /dev/null
+++ b/engines/hadesch/hadesch.cpp
@@ -0,0 +1,922 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+
+#include "common/debug-channels.h"
+#include "common/error.h"
+#include "common/events.h"
+#include "common/ini-file.h"
+#include "common/stream.h"
+#include "common/system.h"
+#include "common/file.h"
+#include "common/keyboard.h"
+#include "common/macresman.h"
+#include "common/util.h"
+#include "common/zlib.h"
+#include "common/config-manager.h"
+
+#include "engines/advancedDetector.h"
+#include "engines/util.h"
+
+#include "graphics/surface.h"
+
+#include "video/smk_decoder.h"
+
+#include "hadesch/pod_file.h"
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+#include "hadesch/pod_image.h"
+
+#include "graphics/palette.h"
+#include "common/memstream.h"
+#include "common/winexe_pe.h"
+#include "common/substream.h"
+#include "common/md5.h"
+#include "graphics/wincursor.h"
+#include "graphics/cursorman.h"
+#include "graphics/maccursor.h"
+
+namespace Hadesch {
+
+HadeschEngine *g_vm;
+
+static const uint32 cursorids[] = {
+	127, 128, 129, // normal and active
+	125, 134, 135, 136, 137, 138, 139, 140, 141, 142, // waiting
+	130, // up arrow
+	131, // left arrow
+	132, // down arrow
+	133, // right arrow
+	// 143:cross, 146:cross
+	146
+};
+
+HadeschEngine::HadeschEngine(OSystem *system, const ADGameDescription *desc)
+	: Engine(system), _desc(desc), _rnd("hadesch") {
+
+	DebugMan.addDebugChannel(kHadeschDebugGeneral, "general", "General issues");
+	DebugMan.addDebugChannel(kHadeschDebugMessagingSystem, "resources", "Resources");
+	DebugMan.addDebugChannel(kHadeschDebugMessagingSystem, "message_system", "Engine message system");
+	DebugMan.addDebugChannel(kHadeschDebugDialogs, "dialogs", "Dialogs");
+
+	g_vm = this;
+	_sceneStartTime = _system->getMillis();
+	_currentTime = 0;
+	_isQuitting = false;
+	_isRestoring = false;
+
+	debug("HadeschEngine::ctor");
+}
+
+HadeschEngine::~HadeschEngine() {
+	debug("HadeschEngine::dtor");
+	DebugMan.clearAllDebugChannels();
+	for (unsigned i = 0; i < _winCursors.size(); i++) {
+		delete _winCursors[i];
+		_winCursors[i] = nullptr;
+	}
+	for (unsigned i = 0; i < _macCursors.size(); i++) {
+		delete _macCursors[i];
+		_macCursors[i] = nullptr;
+	}
+}
+
+void HadeschEngine::setVideoRoom(Common::SharedPtr<VideoRoom> room,
+				 Common::SharedPtr<Handler> handler,
+				 RoomId roomId) {
+	assert(!_isInOptions || _isRestoring);
+	_sceneVideoRoom = room;
+	_sceneHandler = handler;
+	_currentTime = 0;
+	_sceneStartTime = _system->getMillis();
+	if (!_isRestoring) {
+		_persistent._previousRoomId = _persistent._currentRoomId;
+		_persistent._currentRoomId = roomId;
+	}
+	_sceneTimers.clear();
+}
+
+Common::Point HadeschEngine::getMousePos() {
+	return _mousePos;
+}
+
+void HadeschEngine::handleEvent(EventHandlerWrapper event) {
+	event();
+}
+
+void HadeschEngine::newGame() {
+	_persistent = Persistent();
+	_persistent._quest = kCreteQuest;
+	moveToRoom(kWallOfFameRoom);
+}
+
+#if defined(USE_ZLIB)
+
+struct WiseFile {
+	uint start;
+	uint end;
+	uint uncompressedLength;
+};
+
+// Some variants use wise installer. Wise installer use raw zlib compressed files
+// Rather than parsing out the wise structures, we just store offsets for
+// the files we care about
+static const struct {
+	const char *md5;
+	uint setupLength;
+	WiseFile _wdPod;
+	WiseFile _hadeschExe;
+} setups[] = {
+	{
+		// Russian, Windows
+		"9ddf1b0b271426b9d023dbf3edbb1caa",
+		7491209,
+		{0xB8DA2, 0x7246CB, 8691909},
+		{0x4109C, 0xB4628, 1007616}
+	}	
+};
+
+Common::MemoryReadStream *readWiseFile(Common::File &setupFile, const struct WiseFile &wiseFile) {
+	// -4 to skip CRC
+	byte *compressedBuffer = new byte[wiseFile.end - wiseFile.start - 4];
+	byte *uncompressedBuffer = new byte[wiseFile.uncompressedLength];
+	setupFile.seek(wiseFile.start);
+	setupFile.read(compressedBuffer, wiseFile.end - wiseFile.start - 4);
+	if (!Common::inflateZlibHeaderless(uncompressedBuffer, wiseFile.uncompressedLength,
+					   compressedBuffer, wiseFile.end - wiseFile.start - 4)) {
+		debug("wise inflate failed");
+		delete[] compressedBuffer;
+		delete[] uncompressedBuffer;
+		return nullptr;
+	}
+
+	delete[] compressedBuffer;
+	return new Common::MemoryReadStream(uncompressedBuffer, wiseFile.uncompressedLength);
+}
+#endif
+
+Common::ErrorCode HadeschEngine::loadWindowsCursors(Common::PEResources &exe) {
+	for (unsigned i = 0; i < ARRAYSIZE(cursorids); i++) {
+		Graphics::WinCursorGroup *group = Graphics::WinCursorGroup::createCursorGroup(&exe, cursorids[i]);
+
+		if (!group) {
+			debug("Cannot find cursor group %d", cursorids[i]);
+			return Common::kUnsupportedGameidError;
+		}
+
+		_cursors.push_back(group->cursors[0].cursor);
+		_winCursors.push_back(group);
+	}
+
+	return Common::kNoError;
+}
+
+Common::ErrorCode HadeschEngine::loadCursors() {
+	debug("HadeschEngine: loading cursors");
+
+	{
+		Common::PEResources exe = Common::PEResources();
+		if (exe.loadFromEXE("HADESCH.EXE")) {
+			return loadWindowsCursors(exe);
+		}
+	}
+
+	const char *const macPaths[] = {
+		"Hades_-_Copy_To_Hard_Drive/Hades_Challenge/Hades_Challenge_PPC",
+		"Hades - Copy To Hard Drive/Hades Challenge/Hades Challenge PPC"
+	};
+
+	for (uint j = 0; j < ARRAYSIZE(macPaths); ++j) {
+	  	Common::MacResManager resMan = Common::MacResManager();
+		if (!resMan.open(macPaths[j])) {
+			continue;
+		}
+
+		for (unsigned i = 0; i < ARRAYSIZE(cursorids); i++) {
+			Common::SeekableReadStream *stream = resMan.getResource(MKTAG('c','r','s','r'), cursorids[i]);
+			if (!stream) {
+				debug("Couldn't load cursor %d", cursorids[i]);
+				return Common::kUnsupportedGameidError;
+			}
+			Graphics::MacCursor *macCursor = new Graphics::MacCursor();
+			macCursor->readFromStream(*stream);
+			_cursors.push_back(macCursor);
+			delete stream;
+			_macCursors.push_back(macCursor);
+		}
+		return Common::kNoError;
+	}
+
+#if defined(USE_ZLIB)
+	Common::File setupFile;
+	if (setupFile.open("Setup.exe")) {
+		uint len = setupFile.size();
+		Common::String md5 = Common::computeStreamMD5AsString(setupFile, len);
+		for (uint chosenSetup = 0; chosenSetup < ARRAYSIZE(setups); chosenSetup++) {
+			if (setups[chosenSetup].setupLength == len && setups[chosenSetup].md5 == md5) {
+				Common::MemoryReadStream *wdPod = readWiseFile(setupFile, setups[0]._wdPod);
+				if (!wdPod) {
+					debug("wd.pod inflate failed");
+					return Common::kUnsupportedGameidError;
+				}
+				_wdPodFile = Common::SharedPtr<PodFile>(new PodFile("WD.POD"));
+				_wdPodFile->openStore(Common::SharedPtr<Common::SeekableReadStream>(wdPod));
+
+				Common::MemoryReadStream *hadeschExe = readWiseFile(setupFile, setups[0]._hadeschExe);
+				if (!hadeschExe) {
+					debug("hadesch.exe inflate failed");
+					return Common::kUnsupportedGameidError;
+				}
+
+				Common::PEResources exe = Common::PEResources();
+				if (exe.loadFromEXE(hadeschExe)) {
+					return loadWindowsCursors(exe);
+				}
+			}
+		}
+	}
+#endif
+
+	debug("Cannot open hadesch.exe");
+	return Common::kUnsupportedGameidError;
+}
+
+static const char *roomCheats[] = {
+	"",
+	"in",
+	"mo",
+	"ht",
+	"se",
+	"at",
+	"mi",
+	"me",
+	"ar",
+	"tr",
+	"ca",
+	"pr",
+	"th",
+	"cp",
+	"mp",
+	"dw",
+	"mn",
+	"vt",
+	"nr",
+	"htr",
+	"ff",
+	"mm",
+	"hc",
+	"cr",
+	"op"
+};
+
+static const char *itemCheats[] = {
+	"",
+	"",
+	"straw",
+	"stone",
+	"bricks",
+	"message",
+	"key",
+	"decree",
+	"wood",
+	"statue1",
+	"statue2",
+	"statue3",
+	"statue4",
+	"statue",
+	"coin",
+	"potion",
+	"shield",
+	"sword",
+	"bag",
+	"helmet",
+	"sandals",
+	"torch"
+};
+
+bool HadeschEngine::handleGenericCheat(const Common::String &cheat) {
+	if (cheat == "cheatsoff") {
+		_cheatsEnabled = false;
+		return true;
+	}
+
+	for (int i = kIntroRoom; i < kOptionsRoom; i++)
+		if (cheat == roomCheats[i]) {
+			moveToRoom((RoomId) i);
+			getVideoRoom()->disableMouse();
+			return true;
+		}
+
+	if (cheat == "commandson") {
+		getVideoRoom()->enableMouse();
+		return true;
+	}
+
+	if (cheat == "commandsoff") {
+		getVideoRoom()->disableMouse();
+		return true;
+	}
+
+	if (cheat == "cretequest") {
+		_persistent._quest = kCreteQuest;
+		_persistent._roomVisited[kWallOfFameRoom] = true;
+		return true;
+	}
+
+	if (cheat == "troyquest") {
+		_persistent._quest = kTroyQuest;
+		_persistent._roomVisited[kWallOfFameRoom] = true;
+		_persistent._roomVisited[kMinotaurPuzzle] = true;
+
+		_persistent._powerLevel[0] = MAX(_persistent._powerLevel[0], 1);
+		return true;
+	}
+
+	if (cheat == "medusaquest") {
+		_persistent._quest = kMedusaQuest;
+		_persistent._roomVisited[kWallOfFameRoom] = true;
+		_persistent._roomVisited[kMinotaurPuzzle] = true;
+		_persistent._roomVisited[kTrojanHorsePuzzle] = true;
+
+		for (int i = 0; i < 2; i++)
+			_persistent._powerLevel[i] = MAX(_persistent._powerLevel[i], 1);
+		return true;
+	}
+
+	if (cheat == "rescuephilquest") {
+		_persistent._quest = kRescuePhilQuest;
+		_persistent._roomVisited[kWallOfFameRoom] = true;
+		_persistent._roomVisited[kMedusaPuzzle] = true;
+		_persistent._roomVisited[kTrojanHorsePuzzle] = true;
+		_persistent._roomVisited[kMinotaurPuzzle] = true;
+
+		for (int i = 0; i < 3; i++)
+			_persistent._powerLevel[i] = MAX(_persistent._powerLevel[i], 1);
+
+		return true;
+	}
+
+	// TODO: "noquest", "op", "click*", "memory", "showhotspots", "hidehotspots", "showcursor",
+	// giveall, takeall, givekeyanddecree, takekeyanddecree
+
+	for (int i = kStraw; i <= kTorch; i++)
+		if (cheat == Common::String("give") + itemCheats[i]) {
+			_heroBelt->placeToInventory((InventoryItem)i);
+			return true;
+		}
+
+	for (int i = kStraw; i <= kTorch; i++)
+		if (cheat == Common::String("take") + itemCheats[i]) {
+			_heroBelt->removeFromInventory((InventoryItem)i);
+			return true;
+		}
+
+	if (cheat == "hero") {
+		_persistent._gender = kMale;
+		return true;
+	}
+
+	if (cheat == "heroine") {
+		_persistent._gender = kFemale;
+		return true;
+	}
+
+	if (cheat.matchString("powerstrength#")) {
+		_persistent._powerLevel[kPowerStrength] = cheat.substr(13).asUint64();
+		return true;
+	}
+
+	if (cheat.matchString("powerstealth#")) {
+		_persistent._powerLevel[kPowerStealth] = cheat.substr(12).asUint64();
+		return true;
+	}
+
+	if (cheat.matchString("powerwisdom#")) {
+		_persistent._powerLevel[kPowerWisdom] = cheat.substr(11).asUint64();
+		return true;
+	}
+
+	if (cheat.matchString("powerall#")) {
+		int val = cheat.substr(8).asUint64();
+		_persistent._powerLevel[kPowerWisdom] = val;
+		_persistent._powerLevel[kPowerStealth] = val;
+		_persistent._powerLevel[kPowerStrength] = val;
+		return true;
+	}
+
+	return false;
+}
+
+void HadeschEngine::resetOptionsRoom() {
+	_optionsRoom = Common::SharedPtr<VideoRoom>(new VideoRoom("", "", "OPAssets.txt"));
+}
+
+void HadeschEngine::enterOptions() {
+	_isInOptions = true;
+	_optionsEnterTime = _system->getMillis();
+	_sceneVideoRoom->pause();
+	resetOptionsRoom();
+	_optionsHandler = makeOptionsHandler();
+	_optionsHandler->prepareRoom();
+}
+
+void HadeschEngine::enterOptionsCredits() {
+	if (_isInOptions) {
+		_sceneStartTime += _system->getMillis() - _optionsEnterTime;
+	}
+	_isInOptions = true;
+	_optionsEnterTime = _system->getMillis();
+	_optionsRoom = Common::SharedPtr<VideoRoom>(new VideoRoom("CREDITS", "CR", ""));
+	_optionsHandler = makeCreditsHandler(true);
+	_optionsHandler->prepareRoom();
+}
+
+void HadeschEngine::exitOptions() {
+	_isInOptions = false;
+	_sceneStartTime += _system->getMillis() - _optionsEnterTime;
+	_optionsHandler.reset();
+	_optionsRoom.reset();
+	_sceneVideoRoom->unpause();
+}
+
+Common::Error HadeschEngine::run() {
+	debug("HadeschEngine::run");
+
+	const Common::FSNode gameDataDir(ConfMan.get("path"));
+	SearchMan.addSubDirectoryMatching(gameDataDir, "WIN9x");
+
+	Common::ErrorCode err = loadCursors();
+	if (err != Common::kNoError)
+		return err;
+
+	if (!_wdPodFile) {
+		const char *const wdpodpaths[] = {
+			"WIN9x/WORLD/WD.POD", "WD.POD",
+			"Hades_-_Copy_To_Hard_Drive/Hades_Challenge/World/wd.pod",
+			"Hades - Copy To Hard Drive/Hades Challenge/World/wd.pod"};
+		debug("HadeschEngine: loading wd.pod");
+		for (uint i = 0; i < ARRAYSIZE(wdpodpaths); ++i) {
+			Common::SharedPtr<Common::File> file(new Common::File());
+			if (file->open(wdpodpaths[i])) {
+				_wdPodFile = Common::SharedPtr<PodFile>(new PodFile("WD.POD"));
+				_wdPodFile->openStore(file);
+				break;
+			}
+		}
+	}
+
+	if (!_wdPodFile) {
+		debug("Cannot find WD.POD");
+		return Common::kUnsupportedGameidError;
+	}
+
+	_cdScenesPath = "";
+
+	// It's tempting to use SearchMan for this but it
+	// doesn't work because we need to access subdirs based
+	// on cdScenePath
+	const char *const scenepaths[] = {"CDAssets/", "Scenes/"};
+	for (uint i = 0; i < ARRAYSIZE(scenepaths); ++i) {
+		Common::ScopedPtr<Common::File> file(new Common::File());
+		if (file->open(Common::String(scenepaths[i]) + "OLYMPUS/OL.POD")) {
+			_cdScenesPath = scenepaths[i];
+			break;
+		}
+	}
+
+	if (_cdScenesPath == "") {
+		debug("Cannot find OL.POD");
+		return Common::kUnsupportedGameidError;
+	}
+
+	debug("HadeschEngine: intro");
+	initGraphics(kVideoWidth, kVideoHeight);
+
+	_heroBelt = Common::SharedPtr<HeroBelt>(new HeroBelt());
+	_gfxContext = Common::SharedPtr<GfxContext8Bit>(new GfxContext8Bit(2 * kVideoWidth + 10, kVideoHeight + 50));
+	_isInOptions = false;
+
+	debug("HadeschEngine: moving to main loop");
+	_nextRoom = kIntroRoom;
+
+	while (true) {
+		if (_isRestoring) {
+			moveToRoomReal(_persistent._currentRoomId);
+			_isRestoring = false;
+			CursorMan.showMouse(true);
+			CursorMan.replaceCursor(_cursors[3]);
+		} else if (_nextRoom != kInvalidRoom) {
+			moveToRoomReal(_nextRoom);
+			_nextRoom = kInvalidRoom;
+			CursorMan.showMouse(true);
+			CursorMan.replaceCursor(_cursors[3]);
+		}
+
+		if (_isQuitting) {
+			return Common::kNoError;
+		}
+		Common::Event event;
+		bool stopVideo = false;
+		while (_eventMan->pollEvent(event)) {
+			switch (event.type) {
+			case Common::EVENT_QUIT:
+			case Common::EVENT_RETURN_TO_LAUNCHER:
+				return Common::kNoError;
+			case Common::EVENT_LBUTTONDOWN: {
+				if(getVideoRoom()->isMouseEnabled() && getVideoRoom()->isHeroBeltEnabled()
+				   && _heroBelt->isPositionOverHeroBelt(event.mouse)) {
+					debug("handling belt click");
+					_heroBelt->handleClick(event.mouse);
+					break;
+				}
+				const Common::String &q = getVideoRoom()->mapClick(event.mouse);
+				debug("handling click on <%s>", q.c_str());
+				if (getVideoRoom()->isHeroBeltEnabled() && _heroBelt->isHoldingItem())
+					getCurrentHandler()->handleClickWithItem(q, _heroBelt->getHoldingItem());
+				else {
+					getCurrentHandler()->handleAbsoluteClick(event.mouse);
+					getCurrentHandler()->handleClick(q);
+				}
+			}
+				break;
+			case Common::EVENT_LBUTTONUP:
+				getCurrentHandler()->handleUnclick(getVideoRoom()->mapClick(event.mouse), event.mouse);
+				break;
+			case Common::EVENT_KEYDOWN:
+				// TODO: make equivalents for mobile devices. Keyboard is
+				// used for 4 things:
+				//
+				// * Skipping cutscenes (press space)
+				// * Entering name.
+				//      Original requires a non-empty name. We allow an
+				//      empty name.
+				// * Optional save name
+				// * Cheats
+				if (event.kbd.keycode == Common::KEYCODE_SPACE)
+					stopVideo = true;
+				if ((event.kbd.ascii >= 'a' && event.kbd.ascii <= 'z')
+				    || (event.kbd.ascii >= '0' && event.kbd.ascii <= '9')) {
+					_cheat += event.kbd.ascii;
+				}
+				if (event.kbd.keycode == Common::KEYCODE_RETURN) {
+					Common::String cheat = _cheat;
+					_cheat = "";
+					if (cheat == "qazxcdewsrfvbnhytg") {
+						debug("Cheats enabled");
+						_cheatsEnabled = true;
+						break;
+					}
+					if (_cheatsEnabled) {
+						if (handleGenericCheat(cheat))
+							break;
+						if (getCurrentHandler()->handleCheat(cheat))
+							break;
+					}
+				}
+				getCurrentHandler()->handleKeypress(event.kbd.ascii);
+				break;
+			default:
+				break;
+			}
+		}		
+
+		if (_isInOptions) {
+			_currentTime = _system->getMillis() - _optionsEnterTime;
+		} else {
+			_currentTime = _system->getMillis() - _sceneStartTime;
+			for (Common::List<Timer>::iterator it = _sceneTimers.begin();
+			     it != _sceneTimers.end();) {
+				if ((it->period_count != 0 && it->next_time < _currentTime)
+				    || (it->skippable && stopVideo)) {
+					it->next_time = _currentTime + it->period;
+					if (it->period_count != -1) {
+						it->period_count--;
+					}
+					handleEvent(it->event);
+				}
+
+				if (it->period_count == 0) {
+					it = _sceneTimers.erase(it);
+				} else
+					it++;
+			}
+		}
+
+		Common::String oldhotzone = getVideoRoom()->getHotZone();
+		_mousePos = _eventMan->getMousePos();
+		getVideoRoom()->computeHotZone(_currentTime, _mousePos);
+		
+		Common::String newhotzone = getVideoRoom()->getHotZone();
+
+		if (oldhotzone != newhotzone) {
+			getCurrentHandler()->handleMouseOut(oldhotzone);
+			getCurrentHandler()->handleMouseOver(newhotzone);
+		}
+
+		getCurrentHandler()->frameCallback();
+
+		getVideoRoom()->nextFrame(_gfxContext, _currentTime, stopVideo);
+
+		if (getVideoRoom()->getDragged()) {
+			CursorMan.showMouse(true);
+			CursorMan.replaceCursor(getVideoRoom()->getDragged());
+		} else if (getVideoRoom()->isHeroBeltEnabled()
+		    && _heroBelt->isHoldingItem()) {
+			const Graphics::Cursor *cursor = _heroBelt->getHoldingItemCursor(
+				getVideoRoom()->getCursorAnimationFrame(_currentTime));
+			CursorMan.showMouse(true);
+			CursorMan.replaceCursor(cursor);
+		} else {
+			int cursor = getVideoRoom()->getCursor();
+			if (cursor >= 0) {
+				CursorMan.showMouse(true);
+				CursorMan.replaceCursor(_cursors[cursor]);
+			} else {
+				CursorMan.showMouse(true);
+				CursorMan.replaceCursor(_cursors[3]);
+			}
+		}
+
+		_system->updateScreen();
+		_system->delayMillis(15);
+	}
+
+	return Common::kNoError;
+}
+
+Common::RandomSource &HadeschEngine::getRnd() {
+	return _rnd;
+}
+
+bool HadeschEngine::hasFeature(EngineFeature f) const {
+	return
+		f == kSupportsReturnToLauncher ||
+		f == kSupportsLoadingDuringRuntime ||
+		f == kSupportsSavingDuringRuntime ||
+		f == kSupportsChangingOptionsDuringRuntime;
+}
+
+Common::Error HadeschEngine::loadGameStream(Common::SeekableReadStream *stream) {
+	Common::Serializer s(stream, nullptr);
+	if (!_persistent.syncGameStream(s))
+		return Common::kUnknownError;
+
+	_isRestoring = true;
+
+	return Common::kNoError;
+}
+
+Common::Error HadeschEngine::saveGameStream(Common::WriteStream *stream, bool isAutosave) {
+	Common::Serializer s(nullptr, stream);
+	if (isAutosave)
+		_persistent._slotDescription = "Autosave";
+	bool res = _persistent.syncGameStream(s);
+	_persistent._slotDescription = "";
+	return res ? Common::kNoError
+		: Common::kUnknownError;
+}
+
+const Common::String &HadeschEngine::getCDScenesPath() const {
+	return _cdScenesPath;
+}
+
+void HadeschEngine::addTimer(EventHandlerWrapper eventId, int32 start_time, int period, int repeat, bool skippable) {
+	struct Timer timer;
+	assert(!_isInOptions);
+	timer.next_time = start_time + period;
+	timer.period_count = repeat;
+	timer.period = period;
+	timer.event = eventId;
+	timer.skippable = skippable;
+	_sceneTimers.push_back(timer);
+}
+
+void HadeschEngine::addTimer(EventHandlerWrapper eventId, int period, int repeat) {
+	addTimer(eventId, _currentTime, period, repeat, false);
+}
+
+void HadeschEngine::addSkippableTimer(EventHandlerWrapper eventId, int period, int repeat) {
+	addTimer(eventId, _currentTime, period, repeat, true);
+}
+
+void HadeschEngine::cancelTimer(int eventId) {
+	assert(!_isInOptions);
+	for (Common::List<Timer>::iterator it = _sceneTimers.begin();
+	     it != _sceneTimers.end();) {
+		if (it->event == eventId) {
+			it = _sceneTimers.erase(it);
+		} else
+			it++;
+	}
+}
+
+Common::SharedPtr<Handler> HadeschEngine::getCurrentHandler() {
+	return _isInOptions ? _optionsHandler : _sceneHandler;
+}
+	
+Common::SharedPtr<VideoRoom> HadeschEngine::getVideoRoom() {
+	return _isInOptions ? _optionsRoom : _sceneVideoRoom;
+}
+
+void HadeschEngine::moveToRoomReal(RoomId id) {
+	if (_sceneVideoRoom)
+		_sceneVideoRoom->finish();
+
+	_heroBelt->reset();
+
+	switch (id) {
+	case kWallOfFameRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("WALLFAME", "WF", "HTAssets.txt")),
+			     makeWallOfFameHandler(), id);
+		break;
+	case kArgoRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("ARGO", "AR", "ARAssets.txt")),
+			     makeArgoHandler(), id);
+		break;
+	case kCreteRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("CRETE", "CR", "")),
+			     makeCreteHandler(), id);
+			     break;
+	case kOlympusRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("OLYMPUS", "OL", "MOAssets.txt")),
+			     makeOlympusHandler(), id);
+		break;
+	case kMinosPalaceRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("MINOS", "MI", "MIAssets.txt")),
+			     makeMinosHandler(), id);
+		break;
+	case kDaedalusRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("DAEDALUS", "DA", "DWAssets.txt")),
+			     makeDaedalusHandler(), id);
+		break;
+	case kSeriphosRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("SERIPHOS", "SE", "")),
+			     makeSeriphosHandler(), id);
+		break;
+	case kMedIsleRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("MEDISLE", "MI", "")),
+			     makeMedIsleHandler(), id);
+		break;
+	case kMedusaPuzzle:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("MEDUSA", "ME", "")),
+			     makeMedusaHandler(), id);
+		break;
+	case kTroyRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("TROY", "TR", "")),
+			     makeTroyHandler(), id);
+		break;
+	case kTrojanHorsePuzzle:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("TROJAN", "TH", "")),
+			     makeTrojanHandler(), id);
+		break;
+	case kMinotaurPuzzle:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("MINOTAUR", "MM", "")),
+			     makeMinotaurHandler(), id);
+			     break;
+	case kQuiz:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("HADESCH", "HC", "HcAssets.txt")),
+			     makeQuizHandler(), id);
+		break;
+	case kCatacombsRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("CATACOMB", "CA", "CaAssets.txt")),
+			     makeCatacombsHandler(), id);
+		break;
+	case kPriamRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("PRIAM", "PR", "PrAssets.txt")),
+			     makePriamHandler(), id);
+		break;
+	case kAthenaRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("ATHENA", "AT", "")),
+			     makeAthenaHandler(), id);
+		break;
+	case kVolcanoRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("VOLCANO", "VO", "VTAssets.txt")),
+			     makeVolcanoHandler(), id);
+		break;
+	case kRiverStyxRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("NEARR", "NR", "NRAssets.txt")),
+			     makeRiverStyxHandler(), id);
+		break;
+	case kHadesThroneRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("THRONE", "TH", "HTRAsset.txt")),
+			     makeHadesThroneHandler(), id);
+		break;
+	case kCreditsRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("CREDITS", "CR", "")),
+			     makeCreditsHandler(false), id);
+		break;
+	case kIntroRoom:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("INTRO", "IN", "")),
+			     makeIntroHandler(), id);
+		break;
+	case kFerrymanPuzzle:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("FERRY", "FF", "")),
+			     makeFerryHandler(), id);
+		break;
+	case kMonsterPuzzle:
+		setVideoRoom(Common::SharedPtr<VideoRoom>(new VideoRoom("MONSTER", "MM", "")),
+			     makeMonsterHandler(), id);
+		break;
+	default:
+		debug("unknown room %d", id);
+		assert(0);
+		return;
+	}
+
+	_sceneHandler->prepareRoom();
+
+	_persistent._roomVisited[id] = true;
+}
+
+int HadeschEngine::firstAvailableSlot() {
+	for (unsigned slot = 3; ; slot++) {
+		SaveStateDescriptor desc = getMetaEngine().querySaveMetaInfos(_targetName.c_str(), slot);
+		if (desc.getSaveSlot() == -1 && !desc.getWriteProtectedFlag())
+			return slot;
+	}
+}
+
+void HadeschEngine::quit() {
+	_isQuitting = true;
+}
+
+bool HadeschEngine::hasAnySaves() {
+	Common::SaveFileManager *saveFileMan = getSaveFileManager();
+	Common::StringArray filenames;
+	Common::String pattern(getMetaEngine().getSavegameFilePattern(_targetName.c_str()));
+
+	filenames = saveFileMan->listSavefiles(pattern);
+
+	return !filenames.empty();
+}
+
+Common::Array<HadeschSaveDescriptor> HadeschEngine::getHadeschSavesList() {
+	Common::SaveFileManager *saveFileMan = getSaveFileManager();
+	Common::StringArray filenames;
+	Common::String pattern(getMetaEngine().getSavegameFilePattern(_targetName.c_str()));
+
+	filenames = saveFileMan->listSavefiles(pattern);
+
+	Common::Array<HadeschSaveDescriptor> saveList;
+	for (Common::StringArray::const_iterator file = filenames.begin(); file != filenames.end(); ++file) {
+		// Obtain the last 2 digits of the filename, since they correspond to the save slot
+		int slotNum = atoi(file->c_str() + file->size() - 3);
+
+		if (slotNum >= 0) {
+			Common::ScopedPtr<Common::InSaveFile> in(saveFileMan->openForLoading(*file));
+			if (!in) {
+				continue;
+			}
+
+			Common::Serializer s(in.get(), nullptr);
+
+			saveList.push_back(HadeschSaveDescriptor(s, slotNum));
+		}
+	}
+
+	// Sort saves based on slot number.
+	// TODO: change it to chronological save id
+	Common::sort(saveList.begin(), saveList.end(), HadeschSaveDescriptorSlotComparator());
+	return saveList;
+}
+
+void HadeschEngine::deleteSave(int slot) {
+	getMetaEngine().removeSaveState(_targetName.c_str(), slot);
+}
+
+void EventHandlerWrapper::operator()() const {
+  	if (_handler && _eventId == -1)
+		debug("handling anon event");
+	else if (_eventId != 20001 && _eventId != 14006 && _eventId != 15266)
+		debug("handling event %d", _eventId);
+	if (_handler)
+		_handler->operator()();
+	if (_eventId > 0)
+		g_vm->getCurrentHandler()->handleEvent(_eventId);
+}
+
+bool EventHandlerWrapper::operator==(int b) const {
+	return _eventId == b;
+}
+
+} // End of namespace Hadesch
diff --git a/engines/hadesch/hadesch.h b/engines/hadesch/hadesch.h
new file mode 100644
index 0000000000..182e7f0e58
--- /dev/null
+++ b/engines/hadesch/hadesch.h
@@ -0,0 +1,232 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#ifndef HADESCH_HADESCH_H
+#define HADESCH_HADESCH_H
+
+#include "common/random.h"
+#include "common/stream.h"
+#include "common/savefile.h"
+#include "common/list.h"
+
+#include "engines/engine.h"
+#include "engines/savestate.h"
+
+#include "gui/debugger.h"
+#include "graphics/cursor.h"
+#include "hadesch/pod_file.h"
+#include "hadesch/video.h"
+#include "hadesch/enums.h"
+#include "hadesch/event.h"
+#include "hadesch/herobelt.h"
+#include "hadesch/persistent.h"
+
+struct ADGameDescription;
+
+namespace Common {
+class SeekableReadStream;
+class PEResources;
+}
+
+namespace Graphics {
+struct WinCursorGroup;
+struct MacCursor;
+}
+
+namespace Hadesch {
+
+class VideoRoom;
+
+class Handler {
+public:
+	virtual void handleClick(const Common::String &name) = 0;
+	virtual void handleAbsoluteClick(Common::Point pnt) {}
+	virtual bool handleClickWithItem(const Common::String &name, InventoryItem item) {
+		return false;
+	}
+	virtual void handleEvent(int eventId) = 0;
+	virtual void handleMouseOver(const Common::String &name) {}
+	virtual void handleMouseOut(const Common::String &name) {}
+	virtual void frameCallback() {}
+	virtual void handleKeypress(uint16 ucode) {}
+	virtual void prepareRoom() = 0;
+	virtual bool handleCheat(const Common::String &cheat) {
+		return false;
+	}
+	virtual void handleUnclick(const Common::String &name, Common::Point pnt) {}
+	virtual ~Handler() {}
+};
+
+Common::SharedPtr<Hadesch::Handler> makeOlympusHandler();
+Common::SharedPtr<Hadesch::Handler> makeWallOfFameHandler();
+Common::SharedPtr<Hadesch::Handler> makeArgoHandler();
+Common::SharedPtr<Hadesch::Handler> makeCreteHandler();
+Common::SharedPtr<Hadesch::Handler> makeMinosHandler();
+Common::SharedPtr<Hadesch::Handler> makeDaedalusHandler();
+Common::SharedPtr<Hadesch::Handler> makeSeriphosHandler();
+Common::SharedPtr<Hadesch::Handler> makeMedIsleHandler();
+Common::SharedPtr<Hadesch::Handler> makeTroyHandler();
+Common::SharedPtr<Hadesch::Handler> makeMinotaurHandler();
+Common::SharedPtr<Hadesch::Handler> makeQuizHandler();
+Common::SharedPtr<Hadesch::Handler> makeCatacombsHandler();
+Common::SharedPtr<Hadesch::Handler> makePriamHandler();
+Common::SharedPtr<Hadesch::Handler> makeAthenaHandler();
+Common::SharedPtr<Hadesch::Handler> makeVolcanoHandler();
+Common::SharedPtr<Hadesch::Handler> makeRiverStyxHandler();
+Common::SharedPtr<Hadesch::Handler> makeHadesThroneHandler();
+Common::SharedPtr<Hadesch::Handler> makeCreditsHandler(bool inOptions);
+Common::SharedPtr<Hadesch::Handler> makeIntroHandler();
+Common::SharedPtr<Hadesch::Handler> makeFerryHandler();
+Common::SharedPtr<Hadesch::Handler> makeOptionsHandler();
+Common::SharedPtr<Hadesch::Handler> makeMonsterHandler();
+Common::SharedPtr<Hadesch::Handler> makeMedusaHandler();
+Common::SharedPtr<Hadesch::Handler> makeTrojanHandler();
+
+class HadeschEngine : public Engine, Common::NonCopyable {
+public:
+	HadeschEngine(OSystem *syst, const ADGameDescription *desc);
+	~HadeschEngine() override;
+
+	Common::Error run() override;
+
+	bool hasFeature(EngineFeature f) const override;
+
+	bool canLoadGameStateCurrently() override { return true; }
+	bool canSaveGameStateCurrently() override { return true; }
+	Common::Error loadGameStream(Common::SeekableReadStream *stream) override;
+	Common::Error saveGameStream(Common::WriteStream *stream, bool isAutosave = false) override;
+
+	Common::SeekableReadStream *openFile(const Common::String &name, bool addCurrentPath);
+
+	Common::RandomSource &getRnd();
+
+	const Common::String &getCDScenesPath() const;
+
+	Common::SharedPtr<VideoRoom> getVideoRoom();
+
+	void moveToRoom(RoomId id) {
+		_nextRoom = id;
+		_heroBelt->clearHold();
+	}
+
+	void handleEvent(EventHandlerWrapper event);
+	int32 getCurrentTime() {
+		return _currentTime;
+	}
+	Common::Point getMousePos();
+
+	void addTimer(EventHandlerWrapper event, int period, int repeat = 1);
+	void addSkippableTimer(EventHandlerWrapper event, int period, int repeat = 1);
+	void cancelTimer(int eventId);
+
+	Common::SharedPtr<PodFile> getWdPodFile() {
+		return _wdPodFile;
+	}
+
+	RoomId getPreviousRoomId() const {
+		return _persistent._previousRoomId;
+	}
+
+	bool isRoomVisited(RoomId id) const {
+		return _persistent._roomVisited[id];
+	}
+
+	Persistent *getPersistent() {
+		return &_persistent;
+	}
+
+	Common::SharedPtr<Handler> getCurrentHandler();
+
+	Common::SharedPtr<HeroBelt> getHeroBelt() {
+		return _heroBelt;
+	}
+	
+	int firstAvailableSlot();
+
+	void newGame();
+	void enterOptions();
+	void resetOptionsRoom();
+	void exitOptions();
+	void enterOptionsCredits();
+	void quit();
+	bool hasAnySaves();
+
+	Common::Array<HadeschSaveDescriptor> getHadeschSavesList();
+	void deleteSave(int slot);
+
+private:
+	void addTimer(EventHandlerWrapper event, int32 start_time, int period,
+		      int repeat, bool skippable);
+	void moveToRoomReal(RoomId id);
+	void setVideoRoom(Common::SharedPtr<VideoRoom> scene,
+			  Common::SharedPtr<Handler> handler,
+			  RoomId roomId);
+	Common::ErrorCode loadCursors();
+	bool handleGenericCheat(const Common::String &cheat);
+	Common::ErrorCode loadWindowsCursors(Common::PEResources &exe);
+
+	struct Timer {
+		int32 next_time;
+		int32 period;
+		int32 period_count;
+		EventHandlerWrapper event;
+		bool skippable;
+	};
+	const ADGameDescription *_desc;
+
+	Common::RandomSource _rnd;
+
+	Common::String _cdScenesPath;
+	
+	Common::SharedPtr<VideoRoom> _sceneVideoRoom;
+	Common::SharedPtr<Handler> _sceneHandler;
+	Common::SharedPtr<VideoRoom> _optionsRoom;
+	Common::SharedPtr<Handler> _optionsHandler;
+	bool _isInOptions;
+	uint32 _optionsEnterTime;
+	uint32 _sceneStartTime;
+	int32 _currentTime;
+	Common::Array<Graphics::Cursor *> _cursors;
+	Common::List<Timer> _sceneTimers;
+	Common::SharedPtr<PodFile> _wdPodFile;
+	Common::SharedPtr<HeroBelt> _heroBelt;
+	Common::String _cheat;
+	Common::SharedPtr<GfxContext8Bit> _gfxContext;
+	bool _cheatsEnabled;
+	Common::Point _mousePos;
+
+	Persistent _persistent;
+	RoomId _nextRoom;
+	bool _isRestoring;
+  	bool _isQuitting;
+
+	// For freeing purposes
+	Common::Array <Graphics::MacCursor *> _macCursors;
+	Common::Array <Graphics::WinCursorGroup *> _winCursors;
+};
+
+extern HadeschEngine *g_vm;
+
+} // End of namespace Hadesch
+
+#endif
diff --git a/engines/hadesch/herobelt.cpp b/engines/hadesch/herobelt.cpp
new file mode 100644
index 0000000000..00abab15af
--- /dev/null
+++ b/engines/hadesch/herobelt.cpp
@@ -0,0 +1,574 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "common/file.h"
+#include "hadesch/video.h"
+#include "hadesch/pod_image.h"
+#include "hadesch/tag_file.h"
+#include "hadesch/hadesch.h"
+#include "common/system.h"
+#include "video/smk_decoder.h"
+#include "audio/decoders/aiff.h"
+#include "hadesch/pod_file.h"
+#include "hadesch/baptr.h"
+
+namespace Hadesch {
+
+static const int kHeroBeltMinY = 378;
+static const int kHeroBeltMaxY = 471;
+
+static const char *powerSounds[3][2] = {
+	{"g0280nc0", "g0280ng0"},
+	{"g0280nb0", "g0280nf0"},
+	{"g0280ne0", "g0280nh0"},
+};
+
+static Common::Array <PodImage> loadImageArray(const Common::String &name) {
+	Common::SharedPtr<Common::SeekableReadStream> rs(g_vm->getWdPodFile()->getFileStream(name + ".pod"));
+	PodFile pf2(name);
+	pf2.openStore(rs);
+	return pf2.loadImageArray();
+}
+
+static PodImage loadImage(const Common::String &name) {
+	return loadImageArray(name)[0];
+}
+
+static const struct {
+	const char *background;
+	const char *iconNames;
+	const char *icons;
+
+	// TODO: figure out how the original handles
+	// cursors.
+	const char *iconCursors;
+	const char *iconCursorsBright;
+
+	const char *scrollBg;
+	const char *scrollBgHades;
+	const char *scrollTextCrete;
+	const char *scrollTextTroyFemale;
+	const char *scrollTextTroyMale;
+	const char *scrollTextMedusa;
+	const char *scrollTextHades;
+	const char *powerImageWisdom;
+	const char *powerImageStrength;
+	const char *powerImageStealth;
+} assetTable[3] = {
+	// Warm
+	{
+		"g0010oa0",
+		"g0091ba0",
+		"g0091bb0",
+		"g0091bc0",
+		"g0093bc0",
+		"g0015oy0",
+		"g0015oy3",
+		"g0015td0",
+		"g0015te0",
+		"g0015te3",
+		"g0015tf0",
+		"g0015tg0",
+		"g0290oa0",
+		"g0290ob0",
+		"g0290oc0"
+	},
+
+	// Cold
+	{
+		"g0010ob0",
+		"g0092ba0",
+		"g0092bb0",
+		"g0091bc0",
+		"g0093bc0",
+		"g0015oy2",
+		"g0015oy5",
+		"g0015td2",
+		"g0015te2",
+		"g0015te5",
+		"g0015tf2",
+		"g0015tg2",
+		"g0092oa0",
+		"g0092ob0",
+		"g0092oc0"
+	},
+
+	// Cool
+	{
+		"g0010oc0",
+		"g0093ba0",
+		"g0093bb0",
+		"g0091bc0",
+		"g0093bc0",
+		"g0015oy1",
+		"g0015oy4",
+		"g0015td1",
+		"g0015te1",
+		"g0015te4",
+		"g0015tf1",
+		"g0015tg1",
+		"g0093oa0",
+		"g0093ob0",
+		"g0093oc0"
+	}
+};
+
+HeroBelt::HeroBelt() {
+	for (int i = 0; i < 3; i++) {
+		_background[i] = loadImage(assetTable[i].background);
+		_iconNames[i] = loadImageArray(assetTable[i].iconNames);
+		_icons[i] = loadImageArray(assetTable[i].icons);
+		_iconCursors[i] = loadImageArray(assetTable[i].iconCursors);
+		_iconCursorsBright[i] = loadImageArray(assetTable[i].iconCursorsBright);
+		_powerImages[0][i] = loadImageArray(assetTable[i].powerImageStrength);
+		_powerImages[1][i] = loadImageArray(assetTable[i].powerImageStealth);
+		_powerImages[2][i] = loadImageArray(assetTable[i].powerImageWisdom);
+
+		_scrollBg[i] = loadImage(assetTable[i].scrollBg);
+		_scrollBgHades[i] = loadImage(assetTable[i].scrollBgHades);
+		_scrollTextCrete[i] = loadImage(assetTable[i].scrollTextCrete);
+		_scrollTextTroyFemale[i] = loadImage(assetTable[i].scrollTextTroyFemale);
+		_scrollTextTroyMale[i] = loadImage(assetTable[i].scrollTextTroyMale);
+		_scrollTextMedusa[i] = loadImage(assetTable[i].scrollTextMedusa);
+		_scrollTextHades[i] = loadImage(assetTable[i].scrollTextHades);
+	}
+
+	_branchOfLife = loadImageArray("v7150ba0");
+	_overHeroBelt = false;
+	_heroBeltY = kHeroBeltMaxY;
+	_bottomEdge = false;
+	_heroBeltSpeed = 0;
+	_edgeStartTime = 0;
+	_animateItem = kNone;
+	_animateItemTargetSlot = -1;
+	_hotZone = -1;
+	_holdingItem = kNone;
+	_holdingSlot = -1;
+	_highlightTextIdx = -1;
+	_showScroll = false;
+	_hotZones.readHotzones(
+		Common::SharedPtr<Common::SeekableReadStream>(g_vm->getWdPodFile()->getFileStream("HeroBelt.HOT")),
+		true);
+}
+
+void HeroBelt::computeHotZone(int time, Common::Point mousePos, bool mouseEnabled) {
+	bool wasBottomEdge = _bottomEdge;
+
+	_overHeroBelt = false;
+	_bottomEdge = false;
+
+	_mousePos = mousePos;
+
+	if (!mouseEnabled) {
+		return;
+	}
+
+	_bottomEdge = mousePos.y > 460;
+	_overHeroBelt = (_bottomEdge && _heroBeltSpeed < 0) || mousePos.y > _heroBeltY;
+
+	if (!wasBottomEdge && _bottomEdge)
+		_edgeStartTime = time;
+
+	_currentTime = time;
+
+	int wasHotZone = _hotZone;
+	_hotZone = _hotZones.pointToIndex(mousePos);
+	if (_hotZone >= 0 && wasHotZone < 0) {
+		_startHotTime = time;
+	}
+
+	computeHighlight();
+}
+
+static bool isInFrieze() {
+	Persistent *persistent = g_vm->getPersistent();
+	return (persistent->_currentRoomId == kMinotaurPuzzle && persistent->_quest != kCreteQuest)
+		|| (persistent->_currentRoomId == kTrojanHorsePuzzle && persistent->_quest != kTroyQuest)
+		|| (persistent->_currentRoomId == kMedusaPuzzle && persistent->_quest != kMedusaQuest)
+		|| (persistent->_currentRoomId == kFerrymanPuzzle && persistent->_quest != kRescuePhilQuest)
+		|| (persistent->_currentRoomId == kMonsterPuzzle && persistent->_quest != kRescuePhilQuest);
+}
+
+void HeroBelt::render(Common::SharedPtr<GfxContext> context, int time, Common::Point viewPoint) {
+	Persistent *persistent = g_vm->getPersistent();
+	Common::Point beltPoint;
+
+	// TODO: use beltopen hotzone instead?
+	if (persistent->_currentRoomId == kMonsterPuzzle) {
+		_heroBeltY = kHeroBeltMinY;
+	} else {
+		if (_bottomEdge && _heroBeltY == kHeroBeltMaxY
+		    && (time > _edgeStartTime + 500)) {
+			_heroBeltSpeed = -10;
+		}
+
+		if (!_overHeroBelt && _heroBeltY != kHeroBeltMaxY
+		    && _animateItemTargetSlot == -1) {
+			_showScroll = false;
+			_heroBeltSpeed = +10;
+		}
+
+		if (_heroBeltSpeed != 0) {
+			_heroBeltY += _heroBeltSpeed;
+			if (_heroBeltY <= kHeroBeltMinY) {
+				_heroBeltY = kHeroBeltMinY;
+				_heroBeltSpeed = 0;
+			}
+			if (_heroBeltY >= kHeroBeltMaxY) {
+				_heroBeltY = kHeroBeltMaxY;
+				_heroBeltSpeed = 0;
+			}		
+		}
+	}
+
+	_currentTime = time;
+
+	beltPoint = Common::Point(0, _heroBeltY) + viewPoint;
+
+	_background[_colour].render(context, beltPoint);
+
+	if (_animateItem != kNone) {
+		if (_currentTime > _animateItemStartTime + _animItemTime) {
+			_animateItem = kNone;
+			_animateItemTargetSlot = -1;
+			_animItemCallbackEvent();
+		}
+	}
+
+	if (_animateItem != kNone) {
+		Common::Point target = computeSlotPoint(_animateItemTargetSlot, true);
+		Common::Point delta1 = target - _animateItemStartPoint;
+		int elapsed = _currentTime - _animateItemStartTime;
+		double fraction = ((elapsed + 0.0) / _animItemTime);
+		Common::Point deltaPartial = fraction * delta1;
+		Common::Point cur = _animateItemStartPoint + deltaPartial;
+		_iconCursors[_colour][_animateItem - 1].render(
+			context, cur + viewPoint);
+	}
+
+	if (persistent->_currentRoomId == kMonsterPuzzle) {
+		_icons[_colour][kNumberI].render(
+				context,
+				computeSlotPoint(0, false) + viewPoint
+				+ Common::Point(0, 4));
+		_icons[_colour][kNumberII].render(
+				context,
+				computeSlotPoint(1, false) + viewPoint);
+		_icons[_colour][kNumberIII].render(
+				context,
+				computeSlotPoint(2, false) + viewPoint
+				+ Common::Point(0, 4));
+		for (unsigned i = 0; i < 3; i++) {
+			_icons[_colour][_thunderboltFrame].render(
+				context,
+				computeSlotPoint(3 + i, false) + viewPoint);
+		}
+
+		_branchOfLife[_branchOfLifeFrame].render(
+			context, beltPoint);
+
+	} else {
+		for (int i = 0; i < inventorySize; i++) {
+			if (i == _animateItemTargetSlot || i == _holdingSlot
+			    || persistent->_inventory[i] == kNone) {
+				continue;
+			}
+			_icons[_colour][persistent->_inventory[i] - 1].render(
+				context,
+				computeSlotPoint(i, false) + viewPoint);
+		}
+	}
+
+	_icons[_colour][persistent->_hintsAreEnabled ? kActiveHints : kInactiveHints].render(
+		context,
+		computeSlotPoint(6, false) + viewPoint);
+
+	if (isInFrieze())
+		_icons[_colour][kReturnToWall].render(
+			context,
+			computeSlotPoint(7, false) + viewPoint);
+	else
+		_icons[_colour][kQuestScroll].render(
+			context,
+			computeSlotPoint(7, false) + viewPoint);
+
+	_icons[_colour][kOptionsButton].render(
+		context,
+		computeSlotPoint(8, false) + viewPoint);
+
+	if (_highlightTextIdx >= 0) {
+		_iconNames[_colour][_highlightTextIdx].render(
+			context, beltPoint);
+	}
+
+	for (unsigned i = 0; i < ARRAYSIZE(persistent->_powerLevel); i++)
+		if (persistent->_powerLevel[i] > 0) {
+			_powerImages[i][_colour][(int)i == _selectedPower].render(
+				context, beltPoint);
+		}
+
+	if (_showScroll) {
+		PodImage *bg = _scrollBg, *text = nullptr;
+		switch (persistent->_quest) {
+		case kCreteQuest:
+			text = _scrollTextCrete;
+			break;
+		case kTroyQuest:
+			if (persistent->_gender == kMale)
+				text = _scrollTextTroyMale;
+			else
+				text = _scrollTextTroyFemale;
+			break;
+		case kMedusaQuest:
+			text = _scrollTextCrete;
+			break;
+		case kRescuePhilQuest:
+			text = _scrollTextHades;
+			bg = _scrollBgHades;
+			break;
+		case kEndGame:
+		case kNoQuest:
+		case kNumQuests:
+			break;
+		}			
+		bg[_colour].render(context, viewPoint);
+		if (text != nullptr)
+			text[_colour].render(
+				context, viewPoint);
+	}
+}
+
+bool HeroBelt::isPositionOverHeroBelt(Common::Point mousePos) const {
+	return mousePos.y > _heroBeltY;
+}
+
+void HeroBelt::handleClick(Common::Point mousePos) {
+	Persistent *persistent = g_vm->getPersistent();
+	Common::String q = _hotZones.pointToName(mousePos);
+	debug("handling belt click on <%s>", q.c_str());
+	for (int i = 0; i < inventorySize; i++) {
+		if (q == inventoryName(i)) {
+			if (_holdingItem != kNone) {
+				if (persistent->_inventory[i] != kNone &&
+				    _holdingSlot != i)
+					return;
+				persistent->_inventory[_holdingSlot] = kNone;
+				persistent->_inventory[i] = _holdingItem;
+				_holdingItem = kNone;
+				_holdingSlot = -1;
+				return;
+			}
+			if (i == _animateItemTargetSlot
+			    || persistent->_inventory[i] == kNone)
+				return;
+			_holdingItem = persistent->_inventory[i];
+			_holdingSlot = i;
+			return;
+		}
+	}
+
+	if (q == "quest scroll") {
+		if (isInFrieze())
+			g_vm->moveToRoom(kWallOfFameRoom);
+		else
+			_showScroll = true;
+	}
+
+	if (q == "hints") {
+		persistent->_hintsAreEnabled = !persistent->_hintsAreEnabled;
+	}
+
+	if (q == "options") {
+		g_vm->enterOptions();
+	}
+
+	if (q == "strength")
+		clickPower(kPowerStrength);
+	if (q == "stealth")
+		clickPower(kPowerStealth);
+	if (q == "wisdom")
+		clickPower(kPowerWisdom);
+}
+
+void HeroBelt::clickPower(HeroPower pwr) {
+	Persistent *persistent = g_vm->getPersistent();
+	Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+
+	if (persistent->_currentRoomId == kMonsterPuzzle) {
+		_selectedPower = pwr;
+		return;
+	}
+
+	if (persistent->_quest == kRescuePhilQuest)
+		return;
+
+	room->playSound(powerSounds[pwr][!!persistent->_powerLevel[pwr]]);
+}
+
+void HeroBelt::computeHighlight() {
+	Common::String hotZone = _hotZones.indexToName(_hotZone);
+	Persistent *persistent = g_vm->getPersistent();
+	for (int i = 0; i < inventorySize; i++) {
+		if (hotZone == inventoryName(i)) {
+			if (persistent->_currentRoomId == kMonsterPuzzle) {
+				_highlightTextIdx = i < 3 ? kNumberI + i : kLightning1 + i - 3;
+				return;
+			} else {
+				if (persistent->_inventory[i] != kNone &&
+				    _holdingSlot != i) {
+					_highlightTextIdx = persistent->_inventory[i] - 1;
+					return;
+				}
+			}
+		}
+	}
+
+	if (hotZone == "quest scroll") {
+		// TODO: what's with 30 and 28?
+		if (isInFrieze())
+			_highlightTextIdx = kReturnToWall;
+		else if (persistent->_quest == kRescuePhilQuest)
+			_highlightTextIdx = kHadesScroll;
+		else
+			_highlightTextIdx = kQuestScroll;
+		return;
+	}
+
+	if (hotZone == "stealth") {
+		_highlightTextIdx = kPowerOfStealth;
+		return;
+	}
+
+	if (hotZone == "strength") {
+		_highlightTextIdx = kPowerOfStrength;
+		return;
+	}
+
+	if (hotZone == "wisdom") {
+		_highlightTextIdx = kPowerOfWisdom;
+		return;
+	}
+
+	if (hotZone == "hints") {
+		_highlightTextIdx = persistent->_hintsAreEnabled ? kActiveHints : kInactiveHints;
+		return;
+	}
+
+	if (hotZone == "options") {
+		_highlightTextIdx = kOptionsButton;
+		return;
+	}
+
+	_highlightTextIdx = -1;
+}
+
+void HeroBelt::placeToInventory(InventoryItem item, EventHandlerWrapper callbackEvent) {
+	Persistent *persistent = g_vm->getPersistent();
+	unsigned i;
+	for (i = 0; i < inventorySize; i++) {
+		if (persistent->_inventory[i] == kNone)
+			break;
+	}
+
+	if (i == inventorySize) {
+		debug("Out of inventory space");
+		return;
+	}
+
+	persistent->_inventory[i] = item;
+	_animateItem = item;
+	_animItemCallbackEvent = callbackEvent;
+	_animateItemStartPoint = _mousePos;
+	_animateItemTargetSlot = i;
+	_animateItemStartTime = _currentTime;
+	_animItemTime = 2000;
+	_heroBeltSpeed = -10;
+}
+
+void HeroBelt::removeFromInventory(InventoryItem item) {
+	Persistent *persistent = g_vm->getPersistent();
+	for (unsigned i = 0; i < inventorySize; i++) {
+		if (persistent->_inventory[i] == item) {
+			persistent->_inventory[i] = kNone;
+		}
+	}
+
+	if (_holdingItem == item) {
+		_holdingItem = kNone;
+		_holdingSlot = -1;
+	}
+
+	if (_animateItem == item) {
+		_animateItem = kNone;
+		_animateItemTargetSlot = -1;
+	}
+}
+
+void HeroBelt::clearHold() {
+	_holdingItem = kNone;
+	_holdingSlot = -1;
+}
+
+Common::Point HeroBelt::computeSlotPoint(int slot, bool fullyExtended) {
+	Common::Point ret = Common::Point(19, 35);
+	ret += Common::Point(0, (fullyExtended ? kHeroBeltMinY : _heroBeltY));
+	ret += Common::Point(39 * slot, slot % 2 ? 4 : 0);
+	if (slot >= 6) {
+		ret += Common::Point(253, 0);
+	}
+	return ret;
+}
+
+int HeroBelt::getCursor(int time) {
+	Common::String q = _hotZones.indexToName(_hotZone);
+	Persistent *persistent = g_vm->getPersistent();
+	if (q == "")
+		return 0;
+	for (unsigned i = 0; i < inventorySize; i++) {
+		if (q == inventoryName(i)) {
+			if ((int) i != _animateItemTargetSlot
+			    && persistent->_inventory[i] != kNone)
+				return (time - _startHotTime) / kDefaultSpeed % 3;
+			return 0;
+		}
+	}
+
+	return (time - _startHotTime) / kDefaultSpeed % 3;
+}
+
+Common::String HeroBelt::inventoryName(int slot) {
+	return Common::String::format("inventory%d", slot);
+}
+
+bool HeroBelt::isHoldingItem() const {
+	return _holdingItem != kNone;
+}
+
+const Graphics::Cursor *HeroBelt::getHoldingItemCursor(int cursorAnimationFrame) const {
+	if ((cursorAnimationFrame / 2) % 2 == 1)
+		return &_iconCursorsBright[_colour][_holdingItem - 1];
+	else
+		return &_iconCursors[_colour][_holdingItem - 1];
+}
+
+}
diff --git a/engines/hadesch/herobelt.h b/engines/hadesch/herobelt.h
new file mode 100644
index 0000000000..fd559efcb8
--- /dev/null
+++ b/engines/hadesch/herobelt.h
@@ -0,0 +1,134 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#ifndef HADESCH_HEROBELT_H
+#define HADESCH_HEROBELT_H
+
+#include "common/array.h"
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+#include "common/rect.h"
+#include "common/ptr.h"
+#include "hadesch/pod_file.h"
+#include "hadesch/pod_image.h"
+#include "common/hashmap.h"
+#include "common/str.h"
+#include "hadesch/enums.h"
+#include "hadesch/event.h"
+
+namespace Hadesch {
+class HeroBelt {
+public:
+	HeroBelt();
+
+	enum HeroBeltColour {
+		kWarm,
+		kCold,
+		kCool,
+		kNumColours
+	};
+
+	void render(Common::SharedPtr<GfxContext> context, int time, Common::Point viewPoint);
+	void computeHotZone(int time, Common::Point mousePos, bool mouseEnabled);
+	void placeToInventory(InventoryItem item, EventHandlerWrapper callbackEvent = EventHandlerWrapper());
+	void removeFromInventory(InventoryItem item);
+	bool isOverHeroBelt() const {
+		return _overHeroBelt;
+	}
+	bool isPositionOverHeroBelt(Common::Point mousePos) const;
+	void handleClick(Common::Point mousePos);
+	int getCursor(int time);
+	bool isHoldingItem() const;
+	InventoryItem getHoldingItem() const {
+		return _holdingItem;
+	}
+	const Graphics::Cursor *getHoldingItemCursor(int cursorAnimationFrame) const;
+	void clearHold();
+	void setColour(HeroBeltColour colour) {
+		_colour = colour;
+	}
+	void reset() {
+		_colour = HeroBelt::kWarm;
+		_selectedPower = kPowerNone;
+		_branchOfLifeFrame = 0;
+		_thunderboltFrame = kLightning1;
+	}
+	void setBranchOfLifeFrame(int frame) {
+		_branchOfLifeFrame = frame;
+	}
+	void setThunderboltFrame(HeroBeltFrame frame) {
+		_thunderboltFrame = frame;
+	}
+	HeroPower getSelectedStrength() {
+		return _selectedPower;
+	}
+
+private:
+	Common::Point computeSlotPoint(int slot, bool fullyExtended);
+	Common::String inventoryName(int slot);
+	void computeHighlight();
+	void clickPower(HeroPower pwr);
+
+	PodImage _background[kNumColours];
+	Common::Array<PodImage> _iconNames[kNumColours];
+	Common::Array<PodImage> _icons[kNumColours];
+	Common::Array<PodImage> _iconCursors[kNumColours];
+	Common::Array<PodImage> _iconCursorsBright[kNumColours];
+	PodImage _scrollBg[kNumColours];
+	PodImage _scrollBgHades[kNumColours];
+	PodImage _scrollTextCrete[kNumColours];
+	PodImage _scrollTextTroyMale[kNumColours];
+	PodImage _scrollTextTroyFemale[kNumColours];
+	PodImage _scrollTextMedusa[kNumColours];
+	PodImage _scrollTextHades[kNumColours];
+	Common::Array<PodImage> _powerImages[3][kNumColours];
+	Common::Array<PodImage> _branchOfLife;
+
+	Common::Point _mousePos;
+	EventHandlerWrapper _animItemCallbackEvent;
+	HotZoneArray _hotZones;
+	HeroBeltColour _colour;
+	int _heroBeltY;
+	int _heroBeltSpeed;
+	bool _overHeroBelt;
+	bool _bottomEdge;
+	int _edgeStartTime;
+	InventoryItem _animateItem;
+	Common::Point _animateItemStartPoint;
+	int _animateItemTargetSlot;
+	int _animateItemStartTime;
+	int _currentTime;
+	int _animItemTime;
+	int _cursor;
+	int _hotZone;
+	int _startHotTime;
+	int _branchOfLifeFrame;
+	InventoryItem _holdingItem;
+	int _holdingSlot;
+	int _highlightTextIdx;
+	bool _showScroll;
+	HeroPower _selectedPower;
+	HeroBeltFrame _thunderboltFrame;
+};
+}
+#endif
diff --git a/engines/hadesch/hotzone.cpp b/engines/hadesch/hotzone.cpp
new file mode 100644
index 0000000000..c5b3202207
--- /dev/null
+++ b/engines/hadesch/hotzone.cpp
@@ -0,0 +1,201 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "common/file.h"
+
+#include "hadesch/hotzone.h"
+
+namespace Hadesch {
+bool HotZone::isEnabled() const {
+	return _enabled;
+}
+
+void HotZone::setEnabled(bool val) {
+	_enabled = val;
+}
+
+
+void HotZone::setOffset(Common::Point offset) {
+	_offset = offset;
+}
+
+// Following function copied from sword 2.5 and simplified
+bool HotZone::isInside(const Common::Point &point_in) const {
+	Common::Point point = point_in - _offset;
+	int rcross = 0; // Number of right-side overlaps
+
+	// Each edge is checked whether it cuts the outgoing stream from the point
+	for (unsigned i = 0; i < _polygon.size(); i++) {
+		const Common::Point &edgeStart = _polygon[i];
+		const Common::Point &edgeEnd = _polygon[(i + 1) % _polygon.size()];
+
+		// A vertex is a point? Then it lies on one edge of the polygon
+		if (point == edgeStart)
+			return true;
+
+		if ((edgeStart.y > point.y) != (edgeEnd.y > point.y)) {
+			int term1 = (edgeStart.x - point.x) * (edgeEnd.y - point.y) - (edgeEnd.x - point.x) * (edgeStart.y - point.y);
+			int term2 = (edgeEnd.y - point.y) - (edgeStart.y - edgeEnd.y);
+			if ((term1 > 0) == (term2 >= 0))
+				rcross++;
+		}
+	}
+
+	// The point is strictly inside the polygon if and only if the number of overlaps is odd
+	return ((rcross % 2) == 1);
+}
+
+const Common::String &HotZone::getID() const {
+	return _hotid;
+}
+
+int HotZone::getICSH() const {
+	return _icsh;
+}
+
+HotZone::HotZone(const Common::Array<Common::Point> &polygon,
+		 const Common::String &hotid, bool enabled,
+		 int icsh) : _polygon(polygon), _hotid(hotid),
+			     _enabled(enabled), _icsh(icsh) {
+}
+
+void HotZoneArray::readHotzones(Common::SharedPtr<Common::SeekableReadStream> hzFile, bool enable, Common::Point offset) {
+	if (!hzFile) {
+		debug("Invalid hzFile");
+		return;
+	}
+	TagFile tf;
+	tf.openStoreHot(hzFile);
+	Common::ScopedPtr<Common::SeekableReadStream> tcshStream(tf.getFileStream(MKTAG('T', 'C', 'S', 'H')));
+	int hzCnt = tcshStream->readUint32LE();
+	
+	for (int idx = 0; idx < hzCnt; idx++) {
+		Common::SharedPtr<Common::SeekableReadStream> tdshFile(tf.getFileStream(MKTAG('T', 'D', 'S', 'H'), idx));
+		TagFile tdsh;
+		tdsh.openStoreHotSub(tdshFile);
+
+		Common::SharedPtr<Common::SeekableReadStream> tvshFile(tdsh.getFileStream(MKTAG('T', 'V', 'S', 'H')));
+
+		Common::Array<Common::Point> polygon;
+
+		for (int j = 0; j < tvshFile->size() / 8; j++) {
+			uint32 x = tvshFile->readUint32LE();
+			uint32 y = tvshFile->readUint32LE();
+			polygon.push_back(Common::Point(x, y) + offset);
+		}
+
+		Common::SharedPtr<Common::SeekableReadStream> mnshFile(tdsh.getFileStream(MKTAG('M', 'N', 'S', 'H')));
+
+		int mnshSize = mnshFile->size();
+		char *name = new (std::nothrow) char[mnshSize + 1];
+		mnshFile->read(name, mnshSize);
+		name[mnshSize] = 0;
+
+		Common::SharedPtr<Common::SeekableReadStream> icshFile(tdsh.getFileStream(MKTAG('I', 'C', 'S', 'H')));
+
+		int icsh = icshFile->readUint32LE();
+		
+		_hotZones.push_back(HotZone(polygon, name, enable, icsh));
+
+		delete[] name;		
+	}
+}
+
+HotZoneArray::HotZoneArray() {
+}
+
+HotZoneArray::HotZoneArray(Common::SharedPtr<Common::SeekableReadStream> hzFile, bool enable) {
+	readHotzones(hzFile, enable);
+}
+
+void HotZoneArray::setHotzoneEnabled(Common::String const &name, bool val) {
+	for (unsigned i = 0; i < _hotZones.size(); i++) {
+		if (_hotZones[i].getID() == name)
+			_hotZones[i].setEnabled(val);
+	}
+}
+
+int HotZoneArray::pointToIndex(Common::Point point) {
+	for (unsigned i = 0; i < _hotZones.size(); i++) {
+		if (_hotZones[i].isEnabled() && _hotZones[i].isInside(point)) {
+			return i;
+		}
+	}
+
+	return -1;
+}
+
+void HotZoneArray::setHotZoneOffset(const Common::String &name, Common::Point offset) {
+	for (unsigned i = 0; i < _hotZones.size(); i++) {
+		if (_hotZones[i].getID() == name) {
+			_hotZones[i].setOffset(offset);
+		}
+	}
+}
+
+Common::String HotZoneArray::pointToName(Common::Point point) {
+	for (unsigned i = 0; i < _hotZones.size(); i++) {
+		if (_hotZones[i].isEnabled() && _hotZones[i].isInside(point)) {
+			return _hotZones[i].getID();
+		}
+	}
+	return "";
+}
+
+Common::String HotZoneArray::indexToName(int idx) {
+	if (idx >= 0 && idx < (int)_hotZones.size()) {
+		return _hotZones[idx].getID();
+	} else
+		return "";
+}
+
+int HotZoneArray::indexToICSH(int idx) {
+	if (idx < 0 || idx >= (int)_hotZones.size()) {
+		return -1;
+	}
+
+	return _hotZones[idx].getICSH();
+}
+
+int HotZoneArray::indexToCursor(int idx, int frame) {
+	if (idx < 0 || idx >= (int)_hotZones.size()) {
+		return 0;
+	}
+
+	switch (_hotZones[idx].getICSH()) {
+	default:
+		return frame % 3;
+	case 1:
+		return 0;
+	case 2:
+		return 14; // left arrow
+	case 3:
+		return 16; // right arrow
+	case 4:
+		return 13; // up arrow
+	case 5:
+		return 15; // down arrow, never used, just a guess
+	}
+}
+
+}
diff --git a/engines/hadesch/hotzone.h b/engines/hadesch/hotzone.h
new file mode 100644
index 0000000000..f0867cc48b
--- /dev/null
+++ b/engines/hadesch/hotzone.h
@@ -0,0 +1,74 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#ifndef HADESCH_HOTZONE_H
+#define HADESCH_HOTZONE_H
+
+#include "common/ptr.h"
+#include "common/str.h"
+#include "common/rect.h"
+
+#include "hadesch/tag_file.h"
+
+namespace Hadesch {
+
+class HotZone {
+public:
+	HotZone(const Common::Array<Common::Point> &polygon,
+		const Common::String &hotid, bool enabled,
+		int icsh);
+	bool isInside(const Common::Point &pnt) const;
+	const Common::String &getID() const;
+	void load(const TagFile &file, int idx);
+	void setEnabled(bool enabled);
+	bool isEnabled() const;
+	void setOffset(Common::Point offset);
+	int getICSH() const;
+private:
+	Common::String _hotid;
+	Common::Array<Common::Point> _polygon;
+	Common::Point _offset;
+	bool _enabled;
+	int _icsh;
+};
+
+class HotZoneArray {
+public:
+	HotZoneArray();
+	HotZoneArray(Common::SharedPtr<Common::SeekableReadStream> hzFile, bool enable);
+
+	void setHotzoneEnabled(const Common::String &name, bool enabled);
+	void readHotzones(Common::SharedPtr<Common::SeekableReadStream> hzFile,
+			  bool enable, Common::Point offset = Common::Point(0, 0));
+	int pointToIndex(Common::Point point);
+	Common::String pointToName(Common::Point point);
+	void setHotZoneOffset(const Common::String &name, Common::Point offset);
+	Common::String indexToName(int idx);
+	int indexToCursor(int idx, int frame);
+	int indexToICSH(int idx);
+private:
+	Common::Array<HotZone> _hotZones;
+};
+
+}
+#endif
diff --git a/engines/hadesch/metaengine.cpp b/engines/hadesch/metaengine.cpp
new file mode 100644
index 0000000000..41a68317b6
--- /dev/null
+++ b/engines/hadesch/metaengine.cpp
@@ -0,0 +1,59 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "common/system.h"
+#include "common/savefile.h"
+
+#include "hadesch/hadesch.h"
+#include "engines/advancedDetector.h"
+
+class HadeschMetaEngine : public AdvancedMetaEngine {
+public:
+	bool hasFeature(MetaEngineFeature f) const {
+		return
+			(f == kSupportsListSaves) ||
+			(f == kSupportsDeleteSave) ||
+			(f == kSavesSupportMetaInfo) ||
+			(f == kSavesSupportThumbnail) ||
+			(f == kSavesSupportCreationDate) ||
+			(f == kSavesSupportPlayTime) ||
+			(f == kSavesUseExtendedFormat);
+	}
+
+	bool createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const {
+		if (desc)
+			*engine = new Hadesch::HadeschEngine(syst, desc);
+		
+		return desc != 0;
+	}
+
+	const char *getName() const override {
+		return "hadesch";
+	}
+};
+
+#if PLUGIN_ENABLED_DYNAMIC(HADESCH)
+	REGISTER_PLUGIN_DYNAMIC(HADESCH, PLUGIN_TYPE_ENGINE, HadeschMetaEngine);
+#else
+	REGISTER_PLUGIN_STATIC(HADESCH, PLUGIN_TYPE_ENGINE, HadeschMetaEngine);
+#endif
diff --git a/engines/hadesch/module.mk b/engines/hadesch/module.mk
new file mode 100644
index 0000000000..da71321b11
--- /dev/null
+++ b/engines/hadesch/module.mk
@@ -0,0 +1,54 @@
+MODULE := engines/hadesch
+
+MODULE_OBJS = \
+    metaengine.o \
+    pod_file.o \
+    tag_file.o \
+    pod_image.o \
+    video.o \
+    hadesch.o \
+    baptr.o \
+    rooms/olympus.o \
+    rooms/walloffame.o \
+    rooms/argo.o \
+    rooms/crete.o \
+    rooms/minos.o \
+    rooms/daedalus.o \
+    rooms/seriphos.o \
+    rooms/medisle.o \
+    rooms/troy.o \
+    rooms/quiz.o \
+    rooms/minotaur.o \
+    rooms/catacombs.o \
+    rooms/priam.o \
+    rooms/athena.o \
+    rooms/volcano.o \
+    rooms/riverstyx.o \
+    rooms/hadesthrone.o \
+    rooms/credits.o \
+    rooms/intro.o \
+    rooms/ferry.o \
+    rooms/options.o \
+    rooms/monster.o \
+    rooms/monster/projectile.o \
+    rooms/monster/typhoon.o \
+    rooms/monster/cyclops.o \
+    rooms/monster/illusion.o \
+    rooms/medusa.o \
+    rooms/trojan.o \
+    gfx_context.o \
+    ambient.o \
+    herobelt.o \
+    hotzone.o \
+    table.o \
+    persistent.o
+
+DETECT_OBJS += $(MODULE)/detection.o
+
+# This module can be built as a plugin
+ifeq ($(ENABLE_HADESCH), DYNAMIC_PLUGIN)
+PLUGIN := 1
+endif
+
+# Include common rules
+include $(srcdir)/rules.mk
diff --git a/engines/hadesch/persistent.cpp b/engines/hadesch/persistent.cpp
new file mode 100644
index 0000000000..2c36db31fe
--- /dev/null
+++ b/engines/hadesch/persistent.cpp
@@ -0,0 +1,293 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "common/debug-channels.h"
+#include "common/error.h"
+#include "common/events.h"
+#include "common/ini-file.h"
+#include "common/stream.h"
+#include "common/system.h"
+#include "common/file.h"
+#include "common/keyboard.h"
+#include "common/macresman.h"
+#include "common/util.h"
+
+#include "hadesch/persistent.h"
+
+namespace Hadesch {
+
+Persistent::Persistent() {
+	_currentRoomId = kInvalidRoom;
+	_previousRoomId = kInvalidRoom;
+	_quest = kNoQuest;
+	for (unsigned i = 0; i < ARRAYSIZE(_powerLevel); i++)
+		_powerLevel[i] = 0;
+	_hintsAreEnabled = true;
+
+	for (unsigned i = 0; i < ARRAYSIZE(_roomVisited); i++)
+		_roomVisited[i] = false;
+	for (unsigned i = 0; i < ARRAYSIZE(_argoSailedInQuest); i++)
+		for (unsigned j = 0; j < ARRAYSIZE(_argoSailedInQuest[0]); j++)
+			_argoSailedInQuest[i][j] = false;
+	for (unsigned i = 0; i < ARRAYSIZE(_statuesTouched); i++)
+		_statuesTouched[i] = false;
+	for (unsigned i = 0; i < ARRAYSIZE(_statuePhase); i++)
+		_statuePhase[i] = 0;
+
+	_argoSaidTroyFinally = false;
+	_argoSaidCretePort = false;
+
+	_creteShowMerchant = false;
+	_creteShowAtlantisBoat = false;
+	_creteShowHorned = false;
+	_creteShowHornless1 = false;
+	_creteShowHornless2 = false;
+	_creteShowHornless3 = false;
+	_creteShowHornless4 = false;
+	_creteDaedalusRoomAvailable = false;
+	_creteMinosInstructed = false;
+	_creteIntroMerchant = false;
+	_cretePlayedEyeGhostTown = false;
+	_creteIntroAtlantisBoat = false;
+	_creteIntroAtlantisWood = false;
+	_creteSandalsState = SANDALS_NOT_SOLVED;
+	_creteStrongBoxState = BOX_CLOSED;
+	_creteAlchemistExploded = false;
+	_cretePlayedPhilAlchemist = false;
+	_cretePlayedZeusCheckOutThatBox = false;
+	_creteHadesPusnishesPainAndPanic = false;
+	_creteVisitedAfterAlchemistIntro = false;
+	_creteSaidHelenPermanentResident = false;
+
+	_daedalusShowedNote = false;
+
+	_seriphosStrawCartTaken = false;
+	_seriphosPlayedMedusa = false;
+	_seriphosPhilWarnedAthena = false;
+	_seriphosPhilCurtainsItems = false;
+
+	_athenaPuzzleSolved = false;
+	_athenaSwordTaken = false;
+	_athenaShieldTaken = false;
+	_athenaPlayedPainAndPanic = false;
+	_athenaIntroPlayed = false;
+
+	_medisleStoneTaken = false;
+	_medislePlayedPerseusIntro = false;
+	_medisleShowFates = false;
+	_medisleShowFatesIntro = false;
+	for (unsigned i = 0; i < ARRAYSIZE(_statuePhase); i++)
+		_medislePlacedItems[i] = false;
+	_medisleEyeballIsActive = false;
+	_medisleEyePosition = kLachesis;
+	_medisleBagPuzzleState = BAG_NOT_STARTED;
+	_medislePlayedPhilFatesDesc = false;
+
+	_troyPlayAttack = false;
+	_troyWallDamaged = false;
+	_troyShowBricks = false;
+	_troyShowBricks = false;
+	_troyIsDefeated = false;
+	_troyPlayOdysseus = false;
+	_troyKeyAndDecreeState = KEY_AND_DECREE_NOT_GIVEN;
+	_troyMessageIsDelivered = false;
+	_troyCatacombCounter = 0;
+	_troyCatacombsUnlocked = false;
+	_troyPlayedOdysseusCongrats = false;
+	_troyPlayFinish = false;
+
+	for (unsigned i = 0; i < ARRAYSIZE(_catacombVariants); i++)
+		for (unsigned j = 0; j < ARRAYSIZE(_catacombVariants[0]); j++)
+			_catacombVariants[i][j] = 0;
+	for (unsigned i = 0; i < ARRAYSIZE(_catacombPaths); i++)
+		for (unsigned j = 0; j < ARRAYSIZE(_catacombPaths[0]); j++)
+			_catacombPaths[i][j] = kCatacombsHelen;
+	_catacombLevel = kCatacombLevelSign;
+	_catacombLastLevel = kCatacombLevelSign;
+	_catacombDecoderSkullPosition = kCatacombsLeft;
+	_catacombPainAndPanic = false;
+
+	for (unsigned i = 0; i < ARRAYSIZE(_creteTriedHornless); i++)
+		_creteTriedHornless[i] = false;
+	for (unsigned i = 0; i < ARRAYSIZE(_daedalusLabItem); i++)
+		_daedalusLabItem[i] = false;
+
+	_volcanoPainAndPanicIntroDone = false;
+	_volcanoPuzzleState = Persistent::VOLCANO_NO_BOULDERS_THROWN;
+	_volcanoHeyKid = false;
+	_volcanoToStyxCounter = 0;
+
+	_styxCharonUsedPotion = false;
+	_styxCharonUsedCoin = false;
+	_styxAlchemistSaidIntro = false;
+
+	for (unsigned i = 0; i < ARRAYSIZE(_inventory); i++)
+		_inventory[i] = kNone;
+}
+
+void Persistent::clearInventory() {
+	memset(_inventory, 0, sizeof (_inventory));
+}
+
+bool Persistent::isInInventory(InventoryItem item) {
+	for (unsigned i = 0; i < inventorySize; i++) {
+		if (_inventory[i] == item) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+HadeschSaveDescriptor::HadeschSaveDescriptor(Common::Serializer &s, int slot) {
+	s.matchBytes("hadesch", 7);
+	s.syncVersion(0);
+	s.syncString(_heroName);
+	s.syncString(_slotName);
+	s.syncAsByte(_room);
+	_slot = slot;
+}
+
+bool Persistent::syncGameStream(Common::Serializer &s) {
+	if(!s.matchBytes("hadesch", 7))
+		return false;
+	if (!s.syncVersion(0))
+		return false;
+
+	s.syncString(_heroName);
+	s.syncString(_slotDescription);
+
+	s.syncAsByte(_currentRoomId);
+	s.syncAsByte(_previousRoomId);
+
+	s.syncAsByte(_quest);
+	for (unsigned i = 0; i < ARRAYSIZE(_powerLevel); i++)
+		s.syncAsByte(_powerLevel[i]);
+	s.syncAsByte(_hintsAreEnabled);
+
+	for (unsigned i = 0; i < ARRAYSIZE(_roomVisited); i++)
+		s.syncAsByte(_roomVisited[i]);
+	for (unsigned i = 0; i < ARRAYSIZE(_argoSailedInQuest); i++)
+		for (unsigned j = 0; j < ARRAYSIZE(_argoSailedInQuest[0]); j++)
+			s.syncAsByte(_argoSailedInQuest[i][j]);
+	for (unsigned i = 0; i < ARRAYSIZE(_statuesTouched); i++)
+		s.syncAsByte(_statuesTouched[i]);
+	for (unsigned i = 0; i < ARRAYSIZE(_statuePhase); i++)
+		s.syncAsByte(_statuePhase[i]);
+	
+	s.syncAsByte(_argoSaidTroyFinally);
+	s.syncAsByte(_argoSaidCretePort);
+
+	s.syncAsByte(_creteShowMerchant);
+	s.syncAsByte(_creteShowAtlantisBoat);
+	s.syncAsByte(_creteShowHorned);
+	s.syncAsByte(_creteShowHornless1);
+	s.syncAsByte(_creteShowHornless2);
+	s.syncAsByte(_creteShowHornless3);
+	s.syncAsByte(_creteShowHornless4);
+	s.syncAsByte(_creteDaedalusRoomAvailable);
+	s.syncAsByte(_creteMinosInstructed);
+	s.syncAsByte(_creteIntroMerchant);
+	s.syncAsByte(_cretePlayedEyeGhostTown);
+	s.syncAsByte(_creteIntroAtlantisBoat);
+	s.syncAsByte(_creteIntroAtlantisWood);
+	s.syncAsByte(_creteSandalsState);
+	s.syncAsByte(_creteStrongBoxState);
+	s.syncAsByte(_creteAlchemistExploded);
+	s.syncAsByte(_cretePlayedPhilAlchemist);
+	s.syncAsByte(_cretePlayedZeusCheckOutThatBox);
+	s.syncAsByte(_creteHadesPusnishesPainAndPanic);
+	s.syncAsByte(_creteVisitedAfterAlchemistIntro);
+	s.syncAsByte(_creteSaidHelenPermanentResident);
+
+	s.syncAsByte(_daedalusShowedNote);
+
+	s.syncAsByte(_seriphosStrawCartTaken);
+	s.syncAsByte(_seriphosPlayedMedusa);
+	s.syncAsByte(_seriphosPhilWarnedAthena);
+	s.syncAsByte(_seriphosPhilCurtainsItems);
+
+	s.syncAsByte(_athenaPuzzleSolved);
+	s.syncAsByte(_athenaSwordTaken);
+	s.syncAsByte(_athenaShieldTaken);
+	s.syncAsByte(_athenaPlayedPainAndPanic);
+	s.syncAsByte(_athenaIntroPlayed);
+
+	s.syncAsByte(_medisleStoneTaken);
+	s.syncAsByte(_medislePlayedPerseusIntro);
+	s.syncAsByte(_medisleShowFates);
+	s.syncAsByte(_medisleShowFatesIntro);
+	for (unsigned i = 0; i < ARRAYSIZE(_statuePhase); i++)
+		s.syncAsByte(_medislePlacedItems[i]);
+	s.syncAsByte(_medisleEyeballIsActive);
+	s.syncAsByte(_medisleEyePosition);
+	s.syncAsByte(_medisleBagPuzzleState);
+	s.syncAsByte(_medislePlayedPhilFatesDesc);
+
+	s.syncAsByte(_troyPlayAttack);
+	s.syncAsByte(_troyWallDamaged);
+	s.syncAsByte(_troyShowBricks);
+	s.syncAsByte(_troyShowBricks);
+	s.syncAsByte(_troyIsDefeated);
+	s.syncAsByte(_troyPlayOdysseus);
+	s.syncAsByte(_troyKeyAndDecreeState);
+	s.syncAsByte(_troyMessageIsDelivered);
+	s.syncAsByte(_troyCatacombCounter);
+	s.syncAsByte(_troyCatacombsUnlocked);
+	s.syncAsByte(_troyPlayedOdysseusCongrats);
+	s.syncAsByte(_troyPlayFinish);
+
+	for (unsigned i = 0; i < ARRAYSIZE(_catacombVariants); i++)
+		for (unsigned j = 0; j < ARRAYSIZE(_catacombVariants[0]); j++)
+			s.syncAsByte(_catacombVariants[i][j]);
+	for (unsigned i = 0; i < ARRAYSIZE(_catacombPaths); i++)
+		for (unsigned j = 0; j < ARRAYSIZE(_catacombPaths[0]); j++)
+			s.syncAsByte(_catacombPaths[i][j]);
+	s.syncAsByte(_catacombLevel);
+	s.syncAsByte(_catacombLastLevel);
+	s.syncAsByte(_catacombDecoderSkullPosition);
+	s.syncAsByte(_catacombPainAndPanic);
+
+	for (unsigned i = 0; i < ARRAYSIZE(_creteTriedHornless); i++)
+		s.syncAsByte(_creteTriedHornless[i]);
+	for (unsigned i = 0; i < ARRAYSIZE(_daedalusLabItem); i++)
+		s.syncAsByte(_daedalusLabItem[i]);
+
+	s.syncAsByte(_volcanoPainAndPanicIntroDone);
+	s.syncAsByte(_volcanoPuzzleState);
+	s.syncAsByte(_volcanoHeyKid);
+	s.syncAsByte(_volcanoToStyxCounter);
+
+	s.syncAsByte(_styxCharonUsedPotion);
+	s.syncAsByte(_styxCharonUsedCoin);
+	s.syncAsByte(_styxAlchemistSaidIntro);
+
+	for (unsigned i = 0; i < ARRAYSIZE(_inventory); i++)
+		s.syncAsByte(_inventory[i]);
+
+	debug("serialized");
+
+	return true;
+}
+	
+}
diff --git a/engines/hadesch/persistent.h b/engines/hadesch/persistent.h
new file mode 100644
index 0000000000..c5b5c04660
--- /dev/null
+++ b/engines/hadesch/persistent.h
@@ -0,0 +1,188 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+
+#include "common/serializer.h"
+
+#include "hadesch/enums.h"
+
+#ifndef HADESCH_PERSISTENT_H
+#define HADESCH_PERSISTENT_H
+
+namespace Hadesch {
+  
+struct HadeschSaveDescriptor {
+	HadeschSaveDescriptor(Common::Serializer &s, int slot);
+
+	int _slot;
+	Common::String _heroName;
+	Common::String _slotName;
+	RoomId _room;
+};
+
+struct HadeschSaveDescriptorSlotComparator {
+	bool operator()(const HadeschSaveDescriptor &x, const HadeschSaveDescriptor &y) const {
+		return x._slot < y._slot;
+	}
+};
+
+static const int inventorySize = 6;
+
+struct Persistent {
+	// Generic
+	Gender _gender;
+	Common::String _heroName;
+	Common::String _slotDescription; // valid only in saves
+	Quest _quest;
+	int _powerLevel[3];
+	RoomId _currentRoomId;
+	RoomId _previousRoomId;
+	bool _roomVisited[kNumRooms];
+	bool _statuesTouched[kNumStatues];
+	int _statuePhase[kNumStatues];
+	bool _doQuestIntro;
+	InventoryItem _inventory[inventorySize];
+	bool _hintsAreEnabled;
+
+	// Argo
+	bool _argoSailedInQuest[kNumRooms][kNumQuests];
+	bool _argoSaidTroyFinally;
+	bool _argoSaidCretePort;
+
+	// Crete and Minos
+	bool _creteShowMerchant;
+	bool _creteShowAtlantisBoat;
+	bool _creteShowHorned;
+	bool _creteShowHornless1;
+	bool _creteShowHornless2;
+	bool _creteShowHornless3;
+	bool _creteShowHornless4;
+	bool _creteDaedalusRoomAvailable;
+	bool _creteMinosInstructed;
+	bool _creteIntroMerchant;
+	bool _cretePlayedEyeGhostTown;
+	bool _creteTriedHornless[4];
+	bool _creteIntroAtlantisBoat;
+	bool _creteIntroAtlantisWood;
+	bool _creteAlchemistExploded;
+	enum {
+		SANDALS_NOT_SOLVED,
+		SANDALS_SOLVED,
+		SANDALS_TAKEN
+	} _creteSandalsState;
+	enum { BOX_CLOSED, BOX_OPEN,
+	       BOX_OPEN_POTION, BOX_OPEN_NO_POTION } _creteStrongBoxState;
+	bool _cretePlayedPhilAlchemist;
+	bool _cretePlayedZeusCheckOutThatBox;
+	bool _creteHadesPusnishesPainAndPanic;
+	bool _creteVisitedAfterAlchemistIntro;
+	bool _creteSaidHelenPermanentResident;
+
+	// Daedalus
+	bool _daedalusShowedNote;
+	bool _daedalusLabItem[4];
+
+	// Seriphos
+	bool _seriphosStrawCartTaken;
+	bool _seriphosPlayedMedusa;
+	bool _seriphosPhilWarnedAthena;
+	bool _seriphosPhilCurtainsItems;
+
+	// Athena
+	bool _athenaPuzzleSolved;
+	bool _athenaSwordTaken;
+	bool _athenaShieldTaken;
+	bool _athenaPlayedPainAndPanic;
+	bool _athenaIntroPlayed;
+
+	// Medusa Island
+	bool _medisleStoneTaken;
+	bool _medislePlacedItems[5];
+	bool _medislePlayedPerseusIntro;
+	bool _medisleShowFates;
+	bool _medisleShowFatesIntro;
+	bool _medisleEyeballIsActive;
+	FateId _medisleEyePosition;
+	enum {
+		BAG_NOT_STARTED,
+		BAG_STARTED,
+		BAG_SOLVED,
+		BAG_TAKEN
+	} _medisleBagPuzzleState;
+	bool _medislePlayedPhilFatesDesc;
+
+	// Troy
+	bool _troyPlayAttack;
+	bool _troyWallDamaged;
+	bool _troyShowBricks;
+	bool _troyIsDefeated;
+	bool _troyPlayOdysseus;
+	bool _troyMessageIsDelivered;
+	enum {
+		KEY_AND_DECREE_NOT_GIVEN,
+		KEY_AND_DECREE_THROWN,
+		KEY_AND_DECREE_TAKEN
+	} _troyKeyAndDecreeState;
+	int _troyCatacombCounter;
+	bool _troyCatacombsUnlocked;
+	bool _troyPlayedOdysseusCongrats;
+	bool _troyPlayFinish;
+
+	// Catacombs
+	int _catacombVariants[3][3];
+	CatacombsPath _catacombPaths[3][3];
+	CatacombsLevel _catacombLevel;
+	CatacombsPosition _catacombDecoderSkullPosition;
+	CatacombsLevel _catacombLastLevel;
+	bool _catacombPainAndPanic;
+
+	// Volcano
+	bool _volcanoPainAndPanicIntroDone;
+	bool _volcanoHeyKid;
+	enum {
+		VOLCANO_NO_BOULDERS_THROWN,
+		VOLCANO_SQUASHED_PANIC,
+		VOLCANO_BOULDER_ON_VOLCANO,
+		VOLCANO_HELMET_SHOWN
+	} _volcanoPuzzleState;
+	int _volcanoToStyxCounter;
+
+	// River Styx
+	bool _styxCharonUsedPotion;
+	bool _styxCharonUsedCoin;
+	bool _styxAlchemistSaidIntro;
+
+	Persistent();
+
+	bool isInInventory(InventoryItem item);
+	
+	bool isRoomVisited(RoomId id) const {
+		return _roomVisited[id];
+	}
+
+	void clearInventory();
+
+	bool syncGameStream(Common::Serializer &s);
+};
+}
+#endif
diff --git a/engines/hadesch/pod_file.cpp b/engines/hadesch/pod_file.cpp
new file mode 100644
index 0000000000..7d6fe84aa6
--- /dev/null
+++ b/engines/hadesch/pod_file.cpp
@@ -0,0 +1,124 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "common/debug.h"
+#include "common/file.h"
+#include "common/memstream.h"
+
+#include "hadesch/hadesch.h"
+#include "hadesch/pod_file.h"
+
+namespace Hadesch {
+
+PodFile::PodFile(const Common::String &debugName) {
+	_debugName = debugName;
+}
+
+Common::String PodFile::getDebugName() const {
+	return _debugName;
+}
+
+bool PodFile::openStore(const Common::SharedPtr<Common::SeekableReadStream> &parentStream) {
+	byte buf[16];
+	if (!parentStream) {
+		return false;
+	}
+
+	if (parentStream->read(buf, 12) != 12) {
+		return false;
+	}
+
+	if (memcmp(buf, "Pod File\0\0\0\0", 12) != 0 &&
+	    memcmp(buf, "Pod\0file\0\0\0\0", 12) != 0 &&
+	    memcmp(buf, "Pod\0\0\0\0\0\0\0\0\0", 12) != 0) {
+		return false;
+	}
+
+	const uint32 numFiles = parentStream->readUint32LE();
+	uint32 offset = 16 + 16 * numFiles;
+
+	_descriptions.resize(numFiles);
+
+	for (uint i = 0; i < _descriptions.size(); ++i) {
+		parentStream->read(buf, 12);
+		buf[12] = '\0';
+		const uint32 size = parentStream->readUint32LE();
+
+		_descriptions[i].name = (const char *) buf;
+		_descriptions[i].offset = offset;
+		_descriptions[i].size = size;
+		offset += size;
+	}
+
+	_file = parentStream;
+
+	return true;
+}
+
+bool PodFile::openStore(const Common::String &name) {
+	Common::SharedPtr<Common::File> file(new Common::File());
+	if (name.empty() || !file->open(name)) {
+		return false;
+	}
+
+	return openStore(file);
+}
+
+// It's tempting to use substream but substream is not thread safe
+Common::SeekableReadStream *memSubstream(Common::SharedPtr<Common::SeekableReadStream> file,
+					 uint32 offset, uint32 size) {
+	byte *contents = (byte *) malloc(size);
+	if (!contents)
+		return nullptr;
+	file->seek(offset);
+	file->read(contents, size);
+	return new Common::MemoryReadStream(contents, size, DisposeAfterUse::YES);
+}
+
+Common::SeekableReadStream *PodFile::getFileStream(const Common::String &name) const {
+	for (uint j = 0; j < _descriptions.size(); ++j) {
+		const Description &desc = _descriptions[j];
+		if (desc.name.compareToIgnoreCase(name) == 0) {
+			return memSubstream(
+				_file, desc.offset, desc.size);
+		}
+	}
+	debugC(kHadeschDebugResources, "PodFile: %s not found", name.c_str());
+	return nullptr;
+}
+
+Common::Array <PodImage> PodFile::loadImageArray() const {
+  	Common::Array <PodImage> pis;
+
+  	for (int idx = 1; ; idx++) {
+		PodImage pi;
+		if (!pi.loadImage(*this, idx)) {
+			break;
+		}
+		pis.push_back(pi);
+	}
+
+	return pis;
+}
+
+} // End of namespace Hadesch
diff --git a/engines/hadesch/pod_file.h b/engines/hadesch/pod_file.h
new file mode 100644
index 0000000000..1b12cf53b1
--- /dev/null
+++ b/engines/hadesch/pod_file.h
@@ -0,0 +1,64 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#ifndef HADESCH_FILE_MGR_H
+#define HADESCH_FILE_MGR_H
+
+#include "common/array.h"
+#include "common/ptr.h"
+
+namespace Common {
+	class File;
+	class SeekableReadStream;
+}
+
+namespace Hadesch {
+
+class PodImage;
+
+Common::SeekableReadStream *memSubstream(Common::SharedPtr<Common::SeekableReadStream> file,
+					 uint32 offset, uint32 size);
+class PodFile {
+public:
+	PodFile(const Common::String &debugName);
+	bool openStore(const Common::String &name);
+	bool openStore(const Common::SharedPtr<Common::SeekableReadStream> &parentstream);
+
+	Common::SeekableReadStream *getFileStream(const Common::String &name) const;
+	Common::String getDebugName() const;
+	Common::Array <PodImage> loadImageArray() const;
+
+private:
+	struct Description {
+		Common::String name;
+		uint32 offset;
+		uint32 size;
+	};
+	Common::SharedPtr<Common::SeekableReadStream> _file;
+	Common::Array<Description> _descriptions;
+	Common::String _debugName;
+};
+
+} // End of namespace Hadesch
+
+#endif
diff --git a/engines/hadesch/pod_image.cpp b/engines/hadesch/pod_image.cpp
new file mode 100644
index 0000000000..5b8c0eea02
--- /dev/null
+++ b/engines/hadesch/pod_image.cpp
@@ -0,0 +1,271 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "common/debug.h"
+#include "common/stream.h"
+
+#include "hadesch/pod_image.h"
+#include "hadesch/tag_file.h"
+#include "hadesch/baptr.h"
+#include "hadesch/gfx_context.h"
+
+namespace Hadesch {
+PodImage::PodImage() {
+	_w = 0;
+	_h = 0;
+	_pos = Common::Point(0, 0);
+	_ncolors = 0;
+}
+
+PodImage::~PodImage() {
+}
+
+bool PodImage::loadImage(const PodFile &col, int index) {
+	char bufname[256];
+	snprintf (bufname, sizeof(bufname) - 1, "%d", index);
+	Common::SharedPtr<Common::SeekableReadStream> dataStream(col.getFileStream(bufname));
+	if (!dataStream) {
+		return false;
+	}
+
+	Common::SharedPtr<Common::SeekableReadStream> palStream(col.getFileStream("0"));
+	TagFile palTags;
+	if (!palStream || !palTags.openStoreCel(palStream)) {
+		debug("Couldn't open palette");
+		return false;
+	}
+
+	Common::SharedPtr<Common::SeekableReadStream> palTagStream(palTags.getFileStream(MKTAG('P', 'A', 'L', ' ')));
+
+	if (!palTagStream) {
+		debug("Couldn't open PAL palette in image %s", col.getDebugName().c_str());
+		return false;
+	}
+
+	uint palSize = palTagStream->size();
+	if (palSize > 256 * 4) {
+		debug("Palette unexpectedly large");
+		palSize = 256 * 4;
+	}
+
+	_palette = sharedPtrByteAlloc(256 * 4);
+	memset(_palette.get(), 0, 256 * 4);
+	_paletteCursor = sharedPtrByteAlloc(256 * 3);
+	memset(_paletteCursor.get(), 0, 256 * 3);
+
+	palTagStream->read(_palette.get(), palSize);
+	_ncolors = palSize / 4;
+
+	for (int i = 0; i < _ncolors; i++) {
+		int color = _palette.get()[4 * i] & 0xff;
+		
+		_paletteCursor.get()[3 * color    ] = _palette.get()[4 * i + 1];
+		_paletteCursor.get()[3 * color + 1] = _palette.get()[4 * i + 2];
+		_paletteCursor.get()[3 * color + 2] = _palette.get()[4 * i + 3];
+	}
+
+	TagFile dataTags;
+	if (!dataTags.openStoreCel(dataStream)) {
+		debug("Couldn't open data for image %d", index);
+		return false;
+	}
+
+	Common::ScopedPtr<Common::SeekableReadStream> infoTagStream(dataTags.getFileStream(MKTAG('I', 'N', 'F', 'O')));
+
+	if (!infoTagStream) {
+		debug("Couldn't open INFO");
+		return false;
+	}
+
+	if (infoTagStream->size() < 0x1c) {
+		debug("INFO section too small");
+		return false;
+	}
+
+	infoTagStream->skip(0xc);
+	int x = -infoTagStream->readUint32BE();
+	int y = -infoTagStream->readUint32BE();
+	_pos = Common::Point(x,y);
+	_w = infoTagStream->readUint32BE();
+	_h = infoTagStream->readUint32BE();
+
+	// Empty image
+	if (_w < 0 || _h < 0) {
+		_w = 0;
+		_h = 0;
+		_pixels = sharedPtrByteAlloc(1);
+		memset(_pixels.get(), 0, 1);
+		return true;
+	}
+
+	_pixels = sharedPtrByteAlloc(_w * _h);
+	memset(_pixels.get(), 0, _w * _h);
+	// TODO: check this
+	_hotspot = Common::Point(_w / 2, _h / 2);
+
+	Common::ScopedPtr<Common::SeekableReadStream> dataTagStream(dataTags.getFileStream(MKTAG('D', 'A', 'T', 'A')));
+
+	if (!dataTagStream) {
+		debug("Couldn't open DATA in image %s, index %d", col.getDebugName().c_str(), index);
+		return false;
+	}
+
+	int linerem = _w;
+	int line = 0;
+
+	for (int pos = 0; pos < _w * _h && !dataTagStream->eos(); ) {
+		byte rlelen = dataTagStream->readByte();
+		byte rleval = dataTagStream->readByte();
+		if (dataTagStream->eos()) {
+			break;
+		}
+		if (rlelen != 0) {
+			int len = rlelen;
+			if (len > linerem) {
+				len = linerem;
+			}
+			memset(_pixels.get() + pos, rleval, len);
+			linerem -= len;
+			pos += len;
+			continue;
+		}
+
+		if (rleval != 0) {
+			int len = rleval;
+			if (len > linerem) {
+				len = linerem;
+			}
+			dataTagStream->read(_pixels.get() + pos, len);
+			linerem -= len;
+			pos += len;
+			continue;
+		}
+
+		// End of line
+		line++;
+		linerem = _w;
+		pos = line * _w;
+	}
+	
+	return true;
+}
+
+// Naive implementation as it's used very rarely
+// Nearest neighbour, unoptimized
+void PodImage::makeScale(int scale) const {
+	struct ScaledVersion sv;
+	sv._w = _w * scale / 100;
+	sv._h = _h * scale / 100;
+	sv._pixels = sharedPtrByteAlloc(sv._h * sv._w);
+	for (int x = 0; x < sv._w; x++) {
+		int ox = x * _w / sv._w;
+		if (ox >= _w)
+			ox = _w - 1;
+		if (ox < 0)
+			ox = 0;
+		for (int y = 0; y < sv._h; y++) {
+			int oy = y * _h / sv._h;
+			if (oy >= _h)
+				oy = _h - 1;
+			if (oy < 0)
+				oy = 0;
+			sv._pixels.get()[x + y * sv._w] = _pixels.get()[ox + oy * _w];
+		}
+	}
+	_scales[scale] = sv;
+}
+	
+void PodImage::render(Common::SharedPtr<GfxContext> context,
+		      Common::Point offset,
+		      int colourScale,
+		      int scale) const {
+	byte *originalPalette = _palette.get();
+	byte *scaledPalette = nullptr;
+	if (colourScale != 0x100) {
+		scaledPalette = new byte[_ncolors * 4];
+		for (unsigned i = 0; (int)i < _ncolors; i++) {
+			scaledPalette[4 * i] = originalPalette[4 * i];
+			scaledPalette[4 * i + 1] = (originalPalette[4 * i + 1] * colourScale) >> 8;
+			scaledPalette[4 * i + 2] = (originalPalette[4 * i + 2] * colourScale) >> 8;
+			scaledPalette[4 * i + 3] = (originalPalette[4 * i + 3] * colourScale) >> 8;
+		}
+	}
+
+	if (scale == 100)
+		context->blitPodImage(_pixels.get(), _w, _w, _h,
+				      scaledPalette ? scaledPalette : originalPalette, _ncolors, _pos + offset);
+	else {
+		if (!_scales.contains(scale))
+			makeScale(scale);
+		context->blitPodImage(_scales[scale]._pixels.get(), _scales[scale]._w, _scales[scale]._w, _scales[scale]._h,
+				      scaledPalette ? scaledPalette : originalPalette, _ncolors, _pos * (scale / 100.0) + offset);		
+	}
+	if (scaledPalette)
+		delete [] scaledPalette;
+}
+
+Common::Point PodImage::getOffset() const {
+	return _pos;
+}
+
+uint16 PodImage::getWidth() const {
+	return _w;
+}
+
+uint16 PodImage::getHeight() const {
+	return _h;
+}
+
+uint16 PodImage::getHotspotX() const {
+	return _hotspot.x;
+}
+
+uint16 PodImage::getHotspotY() const {
+	return _hotspot.y;
+}
+
+byte PodImage::getKeyColor() const {
+	return 0;
+}
+
+const byte *PodImage::getSurface() const {
+	return _pixels.get();
+}
+
+const byte *PodImage::getPalette() const {
+	return _paletteCursor.get();
+}
+
+byte PodImage::getPaletteStartIndex() const {
+	return 0;
+}
+
+uint16 PodImage::getPaletteCount() const {
+	return 256;
+}
+
+void PodImage::setHotspot(Common::Point hotspot) {
+	_hotspot = hotspot;
+}
+
+}
diff --git a/engines/hadesch/pod_image.h b/engines/hadesch/pod_image.h
new file mode 100644
index 0000000000..c90bfcaa1b
--- /dev/null
+++ b/engines/hadesch/pod_image.h
@@ -0,0 +1,74 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#ifndef HADESCH_POD_IMAGE_H
+#define HADESCH_POD_IMAGE_H
+
+#include "hadesch/pod_file.h"
+#include "common/ptr.h"
+#include "common/rect.h"
+#include "common/hashmap.h"
+#include "hadesch/gfx_context.h"
+#include "graphics/cursor.h"
+
+namespace Hadesch {
+
+class PodImage : public Graphics::Cursor {
+public:
+	PodImage();
+	bool loadImage(const PodFile &col, int index);
+	void render(Common::SharedPtr<GfxContext>, Common::Point offset,
+		    int colourScale = 0x100, int scale = 100) const;
+	bool isValid() const;
+	void setHotspot(Common::Point pnt);
+	Common::Point getOffset() const;
+
+	uint16 getWidth() const override;
+	uint16 getHeight() const override;
+	uint16 getHotspotX() const override;
+	uint16 getHotspotY() const override;
+	byte getKeyColor() const override;
+	const byte *getSurface() const override;
+	const byte *getPalette() const override;
+	byte getPaletteStartIndex() const override;
+	uint16 getPaletteCount() const override;
+
+	~PodImage();
+private:
+	struct ScaledVersion {
+		Common::SharedPtr<byte> _pixels;
+		int _w, _h;
+	};
+	void makeScale(int scale) const;
+
+	mutable Common::HashMap<int, ScaledVersion> _scales;
+	int _w, _h;
+	Common::Point _pos, _hotspot;
+	int _ncolors;
+	Common::SharedPtr<byte> _pixels;
+	Common::SharedPtr<byte> _palette;
+	Common::SharedPtr<byte> _paletteCursor;
+};
+}
+
+#endif
diff --git a/engines/hadesch/rooms/argo.cpp b/engines/hadesch/rooms/argo.cpp
new file mode 100644
index 0000000000..4b40116df1
--- /dev/null
+++ b/engines/hadesch/rooms/argo.cpp
@@ -0,0 +1,346 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+
+namespace Hadesch {
+
+static const char *kIslandNames = "islandnames";
+static const char *kMastHeadAnim = "mastheadanim";
+
+enum {
+	kSkyZ = 10200,
+	kCloudsZ = 10100,
+	kWavesRightZ = 10050,
+	kWavesLeftZ = 10050,
+	kBackgroundZ = 10000,
+	kFlagsZ = 9000,
+	kMastHeadZ = 8000,
+	kChessPieceZ = 701,
+	kIslandNamesZ = 601
+};
+
+static const struct island {
+	const char *hotname;
+	const char *mouseoverAnim; // not mapped
+	const char *nameSound; // not mapped
+	const char *sfxSound; // not mapped
+	RoomId roomId;
+	int zValue;
+} islands[] = {
+	{"Phils", "a1030bh0", "a1030nf0", "a1030ef0", kWallOfFameRoom, 901},
+	{"Medusa", "a1030bf0", "a1030nc0", "a1030ed0", kMedIsleRoom, 901},
+	{"Troy", "a1030bd0", "a1030na0", "a1030eb0", kTroyRoom, 901},
+	{"Seriphos", "a1030be0", "a1030nd0", "a1030ec0", kSeriphosRoom, 801},
+	{"Crete", "a1030bc0", "a1030nb0", "a1030ea0", kCreteRoom, 801},
+	{"Volcano", "a1030bg0", "a1030ne0", "a1030ee0", kVolcanoRoom, 801},
+};
+
+static const int nislands = sizeof(islands) / sizeof(islands[0]);
+
+static const char *intros[] = {
+	"a1150na0",
+	"a1150nb0",
+	"a1150nc0",
+	"a1150nd0",
+	"a1150ne0",
+	"a1150nf0"
+};
+
+static const char *defaultOutros[] = {
+	"a1170na0",
+	"a1170nb0",
+	"a1170nc0",
+	"a1170nd0",
+	"a1170ne0"
+};
+
+enum {
+	kPlayIntro2 = 27001,
+	kPlayIntro3 = 27002,
+	kReturnToIdleEvent = 27003,
+	kIdleEvent = 27008,
+	kOutroFinished = 27009,
+	kOutroFinishedCounter = 1027001
+};
+
+static Common::String
+getOutroName(RoomId dest) {
+	Persistent *persistent = g_vm->getPersistent();
+	Quest quest = persistent->_quest;
+
+	switch (dest) {
+	case kWallOfFameRoom:
+		if (!persistent->_argoSailedInQuest[dest][quest])
+			return "philsfirst";
+		break;
+	case kSeriphosRoom:
+		if (quest == kCreteQuest && !persistent->_argoSailedInQuest[dest][quest])
+			return "seriphoscretetroy";
+		if (quest == kTroyQuest && !persistent->_argoSailedInQuest[dest][quest])
+			return "seriphoscretetroy";
+		if (quest == kMedusaQuest && !persistent->_argoSailedInQuest[dest][quest])
+			return "seriphosperseus";
+		break;
+	case kMedIsleRoom:
+		if (quest == kMedusaQuest && !persistent->_argoSailedInQuest[dest][quest])
+			return "medusabeware";
+		break;
+	case kTroyRoom:
+		if (!persistent->isRoomVisited(kTroyRoom))
+			return "troytenyears";
+		if (quest == kTroyQuest && !persistent->_argoSailedInQuest[dest][quest])
+			return "troyregards";
+		if (quest > kTroyQuest && !persistent->_argoSaidTroyFinally) {
+			persistent->_argoSaidTroyFinally = true;
+			return "troyfinally";
+		}
+		break;
+	case kCreteRoom:
+		if (!persistent->isRoomVisited(kCreteRoom))
+			return "cretedaedalus";
+
+		if (quest != kCreteQuest && !persistent->_argoSaidCretePort)
+			return "creteport";
+		break;
+	case kVolcanoRoom:
+		if (!persistent->isRoomVisited(kVolcanoRoom))
+			return "volcanotopfirst";
+
+		if (quest == kRescuePhilQuest && !!persistent->_argoSailedInQuest[dest][quest])
+			return "volcanotopyoufirst";
+		break;
+
+	default:
+		assert(0);
+	}
+	int rnd = g_vm->getRnd().getRandomNumberRng(0, sizeof(defaultOutros) / sizeof(defaultOutros[0]) - 1);
+	debug("rnd = %d", rnd);
+	return defaultOutros[rnd];
+}
+
+class ArgoHandler : public Handler {
+public:
+	ArgoHandler() {
+		_prevId = kInvalidRoom;
+		_destination = kInvalidRoom;
+	}
+	void handleClick(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		_destination = kInvalidRoom;
+		for (unsigned i = 0; i < nislands; i++) {
+			if (name == islands[i].hotname) {
+				_destination = islands[i].roomId;
+				break;
+			}
+		}
+		if (_destination != kInvalidRoom) {
+		  	Persistent *persistent = g_vm->getPersistent();
+			room->disableMouse();
+			room->stopAnim("idlesound");
+			if (_destination == _prevId) {
+				playMastSound("currentlocation", kOutroFinished);
+				return;
+			}
+
+			_outroCounter = 4;
+			_cloudsMoving = true;
+			_cloudsMoveStart = g_vm->getCurrentTime();
+			playMastSound(getOutroName(_destination), kOutroFinishedCounter);
+			room->playAnimWithSound("wavesleft", "wavesleftSFX", kWavesLeftZ,
+						PlayAnimParams::disappear(),
+						kOutroFinishedCounter);
+			room->playAnimWithSound("wavesright", "wavesrightSFX", kWavesRightZ,
+						PlayAnimParams::disappear(),
+						kOutroFinishedCounter);
+			room->playSound("A1030eG0", kOutroFinishedCounter);
+			persistent->_argoSailedInQuest[_destination][persistent->_quest] = true;
+		}
+	}
+	void handleEvent(int eventId) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		switch (eventId) {
+		case kPlayIntro2:
+			playMastSound("intro2", kPlayIntro3);
+			break;
+		case kPlayIntro3:
+			playMastSound("intro3", kReturnToIdleEvent);
+			break;
+		case kReturnToIdleEvent:
+			room->enableMouse();
+			break;
+		case kOutroFinishedCounter:
+			if (--_outroCounter > 0)
+				break;
+			// Fallthrough
+		case kOutroFinished:
+			room->selectFrame(kMastHeadAnim, kMastHeadZ, 0);
+			g_vm->moveToRoom(_destination);
+			break;
+		case kIdleEvent:
+			playMastSound("idlesound");
+			room->selectFrame(kMastHeadAnim, kMastHeadZ, 1);
+			g_vm->addTimer(kIdleEvent, 30000);
+			break;
+		case 27301:
+			room->playAnimWithSound(kMastHeadAnim, _mastSoundName, kMastHeadZ,
+						PlayAnimParams::keepLastFrame().partial(8, 21), 27303);
+			break;
+		// 27302 was for event chaining and frame keeping
+		case 27303:
+			room->playAnim(kMastHeadAnim, kMastHeadZ,
+				       PlayAnimParams::keepLastFrame().partial(8, 0), _mastHeadEndEvent);
+			break;
+		}
+	}
+	void handleMouseOver(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		for (unsigned i = 0; i < nislands; i++) {
+			if (name == islands[i].hotname) {
+				room->selectFrame(kIslandNames, kIslandNamesZ, i);
+				room->playAnimKeepLastFrame(islands[i].mouseoverAnim, islands[i].zValue);
+				playMastSound(islands[i].nameSound);
+				room->playSoundLoop(islands[i].sfxSound);
+				return;
+			}
+		}
+	}
+	
+	void handleMouseOut(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		for (unsigned i = 0; i < nislands; i++)
+			if (name == islands[i].hotname) {
+				if (_destination != islands[i].roomId) {
+					room->stopAnim(kIslandNames);
+					room->stopAnim(islands[i].mouseoverAnim);
+				}
+				room->stopAnim(islands[i].nameSound);
+				room->stopAnim(islands[i].sfxSound);
+				return;
+			}
+	}
+
+	void prepareRoom() override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		_prevId = g_vm->getPreviousRoomId();
+		room->loadHotZones("argo.HOT");
+		room->addStaticLayer("background", kBackgroundZ);
+		Common::String sky;
+		int chesspiece;
+		Common::String bgsound;
+
+		switch (_prevId) {
+		default:
+			sky = "bluesky";
+			chesspiece = 0;
+			bgsound = "a1180ea0";
+			break;
+		case kSeriphosRoom:
+			sky = "pinksky";
+			chesspiece = 3;
+			bgsound = "A1070eA0";
+			break;
+		case kMedIsleRoom:
+			sky = "mauvesky";
+			chesspiece = 1;
+			bgsound = "a1210ea0";
+			break;
+		case kTroyRoom:
+			sky = "goldsky";
+			chesspiece = 2;
+			bgsound = "a1190eb0";
+			break;
+		case kCreteRoom:
+			sky = "bluesky";
+			chesspiece = 4;
+			bgsound = "a1180ea0";
+			break;
+		case kVolcanoRoom:
+			sky = "pinksky";
+			chesspiece = 5;
+			bgsound = "a1210ea0";
+			break;
+		}
+		room->addStaticLayer(sky, kSkyZ);
+		room->playSoundLoop(bgsound);
+
+		room->selectFrame("chesspiece", kChessPieceZ, chesspiece);
+
+		room->disableMouse();
+		// Originally event 4015
+		if (!persistent->isRoomVisited(kArgoRoom))
+			playMastSound("intro1", kPlayIntro2);
+		else {
+			int rnd = g_vm->getRnd().getRandomNumberRng(0, sizeof(intros) / sizeof(intros[0]) - 1);
+			debug("rnd = %d", rnd);
+			if (rnd == 1 || rnd == 2)
+				rnd = persistent->_gender == kFemale ? 2 : 1;
+			playMastSound(intros[rnd], kReturnToIdleEvent);
+		}
+
+		room->playAnimWithSound("flags", "flagsSFX", kFlagsZ, PlayAnimParams::loop());
+		g_vm->addTimer(kIdleEvent, 30000);
+		g_vm->getHeroBelt()->setColour(HeroBelt::kCool);
+		room->playSound("intromusic");
+		_cloudsMoving = false;
+		cloudMove(0);
+	}
+
+	void frameCallback() override {
+		if (_cloudsMoving) {
+			cloudMove(g_vm->getCurrentTime() - _cloudsMoveStart);
+		}
+	}
+
+	void cloudMove(int cloudMoveTime) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		double div = cloudMoveTime / 15000.0;
+		room->selectFrame("cloudright", kCloudsZ, 0, Common::Point(450, 0) + Common::Point(650, -50) * div);
+		room->selectFrame("cloudmiddle", kCloudsZ, 1, Common::Point(220, 0) + Common::Point(220, -50) * div);
+		room->selectFrame("cloudleft", kCloudsZ, 2, Common::Point(0, 0) + Common::Point(-200, -50) * div);
+	}
+	
+private:
+	void playMastSound(const Common::String &name, int event = -1) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		_mastSoundName = name;
+		_mastHeadEndEvent = event;
+		room->playAnim(kMastHeadAnim, kMastHeadZ, PlayAnimParams::keepLastFrame().partial(1, 8), 27301);
+	}
+
+	RoomId _prevId;
+	RoomId _destination;
+	int _outroCounter;
+	int _cloudsMoveStart;
+	bool _cloudsMoving;
+	int _mastHeadEndEvent;
+	Common::String _mastSoundName;
+};
+
+Common::SharedPtr<Hadesch::Handler> makeArgoHandler() {
+	return Common::SharedPtr<Hadesch::Handler>(new ArgoHandler());
+}
+
+}
diff --git a/engines/hadesch/rooms/athena.cpp b/engines/hadesch/rooms/athena.cpp
new file mode 100644
index 0000000000..07a2c89947
--- /dev/null
+++ b/engines/hadesch/rooms/athena.cpp
@@ -0,0 +1,417 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+#include "hadesch/ambient.h"
+
+namespace Hadesch {
+static const char *kAthenaAnim = "c8060ba0";
+static const char *kLights = "c8110bb0";
+
+// Keep in order
+enum {
+	kWonPuzzle = -2,
+	kNowhere = -1,
+	kBubo = 0,
+	kArtist = 1,
+	kOrator = 2,
+	kScholar = 3,
+	kWarrior = 4
+};
+static const int kNumPuzzleElements = 5;
+static const struct {
+	const char *hotname;
+	int lights[2];
+	int rays[2];
+} puzzleElements[kNumPuzzleElements] = {
+	{"Bubo",  { kArtist, kOrator}, {2, 1}},
+	{"Artist", { kNowhere, kOrator}, {6, 5}},
+	{"Orator", { kArtist, kScholar}, {3, 4}},
+	{"Scholar", { kNowhere, kWarrior}, {8, 9}},
+	{"Warrior", { kScholar, kWonPuzzle}, {7, 10}}
+};
+
+enum {
+	kBackgroundZ = 10000,
+	kLightsZ = 201
+};
+
+enum {
+	kPhilForgettingEnd = 23009,
+	kPhilHardwareFinished = 23012,
+	kIntroFinished = 1023001
+};
+
+class AthenaHandler : public Handler {
+public:
+	AthenaHandler() {
+		_playAreYouForgetting = true;
+		_playAthenaTempleHardware = true;
+		_isPuzzleWon = false;
+		_hintTimerLength = 20000;
+		memset(_puzzleState, 0, sizeof (_puzzleState));
+	}
+
+	void handleClick(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		Quest quest = persistent->_quest;
+		if (name == "Seriphos") {
+			if (quest == kMedusaQuest) {
+				if (persistent->_athenaPuzzleSolved &&
+				    (!persistent->_athenaSwordTaken
+				     || !persistent->_athenaShieldTaken)
+				    && persistent->_hintsAreEnabled
+				    && _playAreYouForgetting) {
+					_playAreYouForgetting = false;
+					room->disableMouse();
+					room->playVideo("c8020ba0", 0,
+							kPhilForgettingEnd,
+							Common::Point(0, 216));
+					return;
+				} else if (persistent->_athenaPuzzleSolved &&
+					   persistent->_athenaSwordTaken &&
+					   persistent->_athenaShieldTaken &&
+					   !persistent->_athenaPlayedPainAndPanic
+					) {
+					persistent->_athenaPlayedPainAndPanic = true;
+					room->disableMouse();
+					room->fadeOut(1000, 23019);
+					return;
+				} else if (persistent->_athenaPuzzleSolved &&
+					   !_playAthenaTempleHardware
+					   && persistent->_hintsAreEnabled) {
+					_playAthenaTempleHardware = false;
+					room->disableMouse();
+					room->playVideo("c8160ba0", 0,
+							kPhilHardwareFinished,
+							Common::Point(0, 216));
+					return;
+				}
+			}
+			g_vm->moveToRoom(kSeriphosRoom);
+			return;
+		}
+
+		if (name == "Athena") {
+			Common::Array<Common::String> videos;
+			if (quest == kMedusaQuest && !persistent->_athenaPuzzleSolved) {
+				videos.push_back(persistent->_gender == kMale ? "c8060wa0" : "c8060wb0");
+			} else {
+				videos.push_back("c8060wc0");
+				videos.push_back("c8060wd0");
+			}
+
+			room->playStatueSMK(kAthenaStatue,
+					    kAthenaAnim,
+					    1101,
+					    videos, 26, 42);
+			return;
+		}
+
+		for (unsigned i = 0; i < kNumPuzzleElements; i++)
+			if (name == puzzleElements[i].hotname) {
+				handlePuzzleClick(i);
+				return;
+			}
+
+		if (name == "Sword") {
+			persistent->_athenaSwordTaken = true;
+			g_vm->getHeroBelt()->placeToInventory(kSword);
+			room->stopAnim("c8130bf0");
+			room->disableHotzone("Sword");
+			room->disableMouse();
+			room->playSound("c8140wa0", 23026);
+			return;
+		}
+
+		if (name == "Shield") {
+			persistent->_athenaShieldTaken = true;
+			g_vm->getHeroBelt()->placeToInventory(kShield);
+			room->stopAnim("c8130be0");
+			room->disableHotzone("Shield");
+			room->disableMouse();
+			room->playSound("c8150wa0", 23027);
+			return;
+		}
+
+		if (name == "Athena's Sword") {
+			room->disableMouse();
+			room->playAnimLoop("c8010oc0", 2101);
+			room->playVideo("c8080wa0", 0, 23043);
+			room->playSound("C8080eA1");
+			return;
+		}
+
+		if (name == "Athena's Shield") {
+			room->disableMouse();
+			room->playAnimLoop("c8010ob0", 2101);
+			room->playVideo("c8070wa0", 0, 23044);
+			room->playSound("C8080eA1");
+			return;
+		}
+	}
+
+	void handleEvent(int eventId) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		switch(eventId) {
+		case kIntroFinished:
+			room->stopAnim(kAthenaAnim);
+			room->playAnim("c8110ba0", 0, PlayAnimParams::disappear(), 23035);
+			room->enableMouse();
+			_hintTimerLength = 20000;
+			rescheduleHintTimer();
+			break;
+		case kPhilForgettingEnd:
+		case kPhilHardwareFinished:
+		case 23026:
+		case 23027:
+			room->enableMouse();
+			break;
+		case 23007:
+			handleEvent(23010);
+			_hintTimerLength = 40000;
+			rescheduleHintTimer();
+			break;
+		case 23008:
+			room->playAnim("c8140ba0", 1101, PlayAnimParams::disappear(), 23015);
+			room->playAnim("c8150ba0", 1101, PlayAnimParams::disappear(), 23016);
+			room->playSound("c8130ma0", 23020);
+			room->playSound("c8130eb0");
+			room->playSound("c8130ec0");
+			break;
+		case 23010:
+			if (persistent->_hintsAreEnabled && room->isMouseEnabled()) {
+				room->disableMouse();
+				room->playVideo("c8170ba0", 0, 23011, Common::Point(0, 216));
+			}
+			break;
+		case 23011:
+			room->enableMouse();
+			break;
+		case 23015:
+			room->playAnimKeepLastFrame("c8130bf0", 1101, 23017);
+			room->playSound("c8130ef0");
+			break;
+		case 23016:
+			room->playAnimKeepLastFrame("c8130be0", 1101, 23018);
+			room->playSound("c8130ee0");
+			break;
+		case 23017:
+			room->enableHotzone("Sword");
+			room->enableMouse();
+			break;
+		case 23018:
+			room->enableHotzone("Shield");
+			room->enableMouse();
+			break;
+		case 23019:
+			room->resetFade();
+			room->disableHeroBelt();
+			room->resetLayers();
+			room->addStaticLayer("c8180pa0", 9000);
+			room->playSound("g0261ma0", 23031);
+			room->playSound(persistent->_gender == kMale ? "c8180wa0" : "c8180wb0", 23029);
+			break;
+		case 23029:
+			room->playSound("c8180wc0", 23030);
+			break;
+		case 23030:
+			room->playVideo("c8180ba0", 0, 23032);
+			break;
+		case 23031:
+			break;
+		case 23032:
+			room->selectFrame("c8180bb0", 101, 0);
+			g_vm->moveToRoom(kSeriphosRoom);
+			break;
+		// TODO: lighting up of the beam: 23035/23036 are for lighting up
+		case 23035:
+		case 23036:
+			room->selectFrame(LayerId(kLights, 0, "source"), kLightsZ, 0);
+			/* Fallthrough */
+		case 23037:
+			room->playAnimLoop("c8110bc0", 211);
+			room->enableHotzone("Athena's Sword");
+			room->enableHotzone("Athena's Shield");
+			for (unsigned i = 0; i < kNumPuzzleElements; i++)
+				room->enableHotzone(puzzleElements[i].hotname);
+			break;
+		case 23020:
+			room->playAnim("c8130bd0", 0, PlayAnimParams::disappear(), 23041);
+			/*Fallthrough */
+		case 23038:
+			g_vm->addTimer(23040, 640);
+			g_vm->addTimer(23039, 40, 15);
+			break;
+		case 23039:
+			//TODO progressive beams
+			break;
+		case 23040:
+			for (unsigned i = 0; i < 12; i++)
+				room->stopAnim(LayerId(kLights, i, "internal"));
+			room->stopAnim(LayerId(kLights, 0, "source"));
+			room->stopAnim("c8110bc0");
+			break;
+		case 23043:
+			room->stopAnim("c8010oc0");
+			room->enableMouse();
+			break;
+		case 23044:
+			room->stopAnim("c8010ob0");
+			room->enableMouse();
+			break;
+		}
+	}
+
+	void prepareRoom() override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		Quest quest = persistent->_quest;
+		room->loadHotZones("Athena.HOT", false);
+		room->addStaticLayer("c8010pa0", kBackgroundZ);
+		room->addStaticLayer("c8010ta0", 601);
+		room->enableHotzone("Athena");
+		room->enableHotzone("Seriphos");
+
+		if (quest == kMedusaQuest && !persistent->_athenaPuzzleSolved) {
+			persistent->_athenaIntroPlayed = true;
+			room->disableMouse();
+			room->playVideo(persistent->_gender == kMale ? "c8040wa0" : "c8040wb0",
+					1101, kIntroFinished);
+			room->playAnim(kAthenaAnim, 1101, PlayAnimParams::loop());
+			room->playSound("c8040ma0", 23013);
+		}
+
+		if (!persistent->_athenaShieldTaken) {
+			if (persistent->_athenaPuzzleSolved) {
+				room->selectFrame("c8130be0", 1101, 4);
+				room->enableHotzone("Shield");
+			} else {
+				room->selectFrame("c8150ba0", 1101, 0);
+			}
+		}
+
+		if (!persistent->_athenaSwordTaken) {
+			if (persistent->_athenaPuzzleSolved) {
+				room->selectFrame("c8130bf0", 1101, 7);
+				room->enableHotzone("Sword");
+			} else {
+				room->selectFrame("c8140ba0", 1101, 0);
+			}
+		}
+
+		room->playAnimLoop("c8030ba0", 201);
+		g_vm->getHeroBelt()->setColour(HeroBelt::kCool);
+	}
+private:
+	void rescheduleHintTimer() {
+		Persistent *persistent = g_vm->getPersistent();
+
+		g_vm->cancelTimer(23007);
+		if (!persistent->_athenaPuzzleSolved)
+			g_vm->addTimer(23007, _hintTimerLength);
+	}
+
+	void handlePuzzleClick(int num) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+
+		rescheduleHintTimer();
+
+		_puzzleState[num] = (_puzzleState[num] + 1) % 3;
+		memset(_isPuzzleLit, 0, sizeof (_isPuzzleLit));
+		_isPuzzleLit[0] = true;
+		bool done = false;
+		while (!done) {
+			done = true;
+			for (unsigned i = 0; i < kNumPuzzleElements; i++)
+				if (_puzzleState[i] != 0 && _isPuzzleLit[i]
+				    && puzzleElements[i].lights[_puzzleState[i] - 1] >= 0
+				    && !_isPuzzleLit[puzzleElements[i].lights[_puzzleState[i] - 1]]) {
+					_isPuzzleLit[puzzleElements[i].lights[_puzzleState[i] - 1]] = true;
+					done = false;
+				}
+		}
+		for (unsigned i = 0; i < kNumPuzzleElements; i++)
+			if (_puzzleState[i] != 0 && _isPuzzleLit[i]
+			    && puzzleElements[i].lights[_puzzleState[i] - 1] == kWonPuzzle) {
+				_isPuzzleWon = true;
+				break;
+			}
+		for (unsigned i = 0; i < kNumPuzzleElements; i++)
+			if (!_isPuzzleLit[i])
+				_puzzleState[i] = 0;
+		for (unsigned i = 0; i < 11; i++)
+			room->stopAnim(LayerId(kLights, i, "internal"));
+		for (unsigned i = 0; i < kNumPuzzleElements; i++)
+			if (_puzzleState[i] != 0 && _isPuzzleLit[i]) {
+				int ray = puzzleElements[i].rays[_puzzleState[i] - 1];
+				if (ray <= 0)
+					continue;
+				room->selectFrame(LayerId(kLights, ray, "internal"), kLightsZ, ray);
+			}
+
+		if (_isPuzzleLit[kArtist] && _puzzleState[kArtist] == 1)
+			room->playAnimLoop("c8120bg0", 601);
+		else
+			room->stopAnim("c8120bg0");
+		if (_isPuzzleLit[kScholar] && _puzzleState[kScholar] == 1)
+			room->playAnimLoop("c8120bg1", 601);
+		else
+			room->stopAnim("c8120bg1");
+
+		if (_isPuzzleLit[num] && _puzzleState[num])
+			room->playSound("c8120ea0");
+
+		if (_isPuzzleWon) {
+			room->selectFrame(LayerId(kLights, 11, "internal"), kLightsZ, 11);
+			room->playSound("C8130eA0");
+			g_vm->addTimer(23008, 1000);
+			room->disableHotzone("Athena's Sword");
+			room->disableHotzone("Athena's Shield");
+			for (unsigned i = 0; i < kNumPuzzleElements; i++)
+				room->disableHotzone(puzzleElements[i].hotname);
+			room->disableMouse();
+			persistent->_athenaPuzzleSolved = true;
+		}
+
+		room->playSoundLoop(
+			persistent->_quest != kMedusaQuest || persistent->_athenaPuzzleSolved
+			? "c8010ea0" : "c8110ea0");
+	}
+	bool _playAreYouForgetting;
+	bool _playAthenaTempleHardware;
+	bool _isPuzzleLit[kNumPuzzleElements];
+	int _puzzleState[kNumPuzzleElements];
+	bool _isPuzzleWon;
+	int _hintTimerLength;
+};
+
+Common::SharedPtr<Hadesch::Handler> makeAthenaHandler() {
+	return Common::SharedPtr<Hadesch::Handler>(new AthenaHandler());
+}
+
+}
diff --git a/engines/hadesch/rooms/catacombs.cpp b/engines/hadesch/rooms/catacombs.cpp
new file mode 100644
index 0000000000..84fab752a5
--- /dev/null
+++ b/engines/hadesch/rooms/catacombs.cpp
@@ -0,0 +1,477 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+#include "hadesch/ambient.h"
+
+namespace Hadesch {
+
+static const char *caTxtNames[] = {
+	"CaLeft.txt",
+	"CaCenter.txt",
+	"CaRight.txt"
+};
+
+static const char *skullHotzones[] = {
+	"LSkull",
+	"CSkull",
+	"RSkull"
+};
+
+static const char *torchHotzones[] = {
+	"LTorch",
+	"CTorch",
+	"RTorch"
+};
+
+static const char *signNames[] = {
+	"SignToHelen",
+	"SignToGuards",
+	"SignToPainPanic"
+};
+
+static const char *musicNames[] = {
+	"MusicHelen",
+	"MusicGuard",
+	"MusicPainPanic"
+};
+
+static const char *painSounds[] = {
+	"SndPainBedtime",
+	"SndPanicBoneHead",
+	"SndPainRecognize"
+};
+
+static const char *painSounds2[] = {
+	"SndPanicLightsOut",
+	"SndPainByeBye",
+	"SndPanicMaybeHit"
+};
+
+enum {
+	kBackgroundCenterZ = 10001,
+	kBackgroundZ = 10000
+};
+
+enum {
+	// Originally 22014 for all 3 but I'd rather avoid passing extra args around
+	kL1TrochLitLeft = 1022001,
+	kL1TrochLitCenter = 1022002,
+	kL1TrochLitRight = 1022003,
+	kBonkVideoFinished = 1022004
+};
+
+class CatacombsHandler : public Handler {
+public:
+	CatacombsHandler() {
+		_philWarnedTorch = false;
+		_philBangPlayed = false;
+	}
+
+	void handleClick(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		int level = persistent->_catacombLevel;
+
+		if (name == "LExit") {
+			handleExit(kCatacombsLeft);
+			return;
+		}
+		if (name == "CExit") {
+			handleExit(kCatacombsCenter);
+			return;
+		}
+		if (name == "RExit") {
+			handleExit(kCatacombsRight);
+			return;
+		}
+		if (level == 0 && (name == "LTorch" || name == "CTorch" || name == "RTorch")) {
+			g_vm->getHeroBelt()->placeToInventory(kTorch);
+			room->stopAnim(caVariantGet(_torchPosition, "TorchNormal"));
+			return;
+		}
+
+		if (level == 1 && name == "LTorch") {
+			lightTorchL1(kCatacombsLeft);
+			return;
+		}
+
+		if (level == 1 && name == "CTorch") {
+			lightTorchL1(kCatacombsCenter);
+			return;
+		}
+
+		if (level == 1 && name == "RTorch") {
+			lightTorchL1(kCatacombsRight);
+			return;
+		}
+
+		if (name == "LSkull" || name == "CSkull" || name == "RSkull") {
+			_decoderPosition = 0;
+			renderDecoder();
+			if (!_philBangPlayed) {
+				_philBangPlayed = false;
+				room->playSound("SndBigBang", 22012);
+			}
+			return;
+		}
+
+		if (name == "DecoderDown" && _decoderPosition < 6) {
+			_decoderPosition++;
+			renderDecoder();
+			room->playAnim("AnimDecoderArrows", 149, PlayAnimParams::disappear().partial(0, 0));
+			return;
+		}
+
+		if (name == "DecoderUp" && _decoderPosition > 0) {
+			_decoderPosition--;
+			renderDecoder();
+			room->playAnim("AnimDecoderArrows", 149, PlayAnimParams::disappear().partial(1, 1));
+			return;
+		}
+	}
+
+	bool handleClickWithItem(const Common::String &name, InventoryItem item) override {
+		Persistent *persistent = g_vm->getPersistent();
+		int level = persistent->_catacombLevel;
+		if (item == kTorch && level == 1) {
+			if (name == "LTorch") {
+				lightTorchL1(kCatacombsLeft);
+				return true;
+			}
+
+			if (name == "CTorch") {
+				lightTorchL1(kCatacombsCenter);
+				return true;
+			}
+
+			if (name == "RTorch") {
+				lightTorchL1(kCatacombsRight);
+				return true;
+			}
+		}
+		return false;
+	}
+
+
+	void handleEvent(int eventId) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		int level = persistent->_catacombLevel;
+		switch(eventId) {
+		case kL1TrochLitLeft:
+		case kL1TrochLitCenter:
+		case kL1TrochLitRight: {
+			CatacombsPosition side = (CatacombsPosition) (eventId - kL1TrochLitLeft + kCatacombsLeft);
+			bool isHelen = persistent->_catacombPaths[1][side] == kCatacombsHelen;
+			room->playAnimLoop(
+				caVariantGet(side, isHelen ? "TorchLong" : "TorchNormal"),
+				caVariantGet(side, "TorchZ").asUint64());
+			break;
+		}
+		case 22009:
+			room->playVideo("PhilQuickNameThatTune", 0);
+			break;			
+		case 22012:
+			room->playVideo("PhilWowLowOnTroops", 0);
+			break;
+		case 22016:
+			room->playSound("SndGuardTrapDoorOpen", 22017);
+			break;
+		case 22017:
+			room->playSound("SndGuardLaugh", 22018);
+			break;
+		case 22018:
+			room->playSound(
+				Common::String::format("T3220w%c0",
+						       g_vm->getRnd().getRandomNumberRng('A', 'C')),
+				22019);
+			break;
+		case 22019:
+			room->playSound("SndGuardTrapDoorClose", 22020);
+			break;
+		case 22020:
+			persistent->_catacombLevel = kCatacombLevelSign;
+			g_vm->moveToRoom(kTroyRoom);
+			break;
+		case 22022:
+			room->playSound(painSounds2[level], 22023);
+			persistent->_catacombLevel = kCatacombLevelSign;
+			break;
+		case 22023:
+			room->playVideo("MovPainPanicBonk", 103, kBonkVideoFinished);
+			break;
+		case kBonkVideoFinished:
+			g_vm->moveToRoom(kTroyRoom);
+			break;
+		}
+	}
+
+	void handleMouseOver(const Common::String &name) override {
+		Persistent *persistent = g_vm->getPersistent();
+		int level = persistent->_catacombLevel;
+
+		if (level == 2) {
+			if (name == "LExit") {
+				playTune(kCatacombsLeft);
+				return;
+			}
+			if (name == "CExit") {
+				playTune(kCatacombsCenter);
+				return;
+			}
+			if (name == "RExit") {
+				playTune(kCatacombsRight);
+				return;
+			}
+		}
+	}
+
+	void handleMouseOut(const Common::String &name) override {
+		Persistent *persistent = g_vm->getPersistent();
+		int level = persistent->_catacombLevel;
+
+		if (level == 2 && (name == "LExit" || name == "CExit" || name == "RExit"))
+			stopTune();
+	}
+
+	void prepareRoom() override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		CatacombsLevel level = persistent->_catacombLevel;
+
+		persistent->_catacombLastLevel = level;
+
+		room->playSoundLoop("T3010eA0");
+
+		if (persistent->_catacombPainAndPanic) {
+			persistent->_catacombPainAndPanic = false;
+			room->addStaticLayer("DeadEndBackground", 10001);
+			room->playSound("SndPainPanicStinger", 22022);
+			room->playSound(painSounds[persistent->_catacombLevel]);
+			return;
+		}
+		
+		if (level == 0)
+			room->loadHotZones("CaDecode.HOT", false);
+		room->playSoundLoop("T3010eA0");
+		// TODO: tremmors
+		// TODO: handle timer
+		g_vm->addTimer(22007, level == 2 ? 30000 : 40000, -1);
+
+		for (int i = 0; i < 3; i++)
+			_caMapTxt[i] = TextTable(
+				Common::SharedPtr<Common::SeekableReadStream>(room->openFile(caTxtNames[i])), 13);
+
+		if (persistent->_catacombPaths[0][0] == kCatacombsHelen
+		    && persistent->_catacombPaths[1][0] == kCatacombsHelen) {
+			for (int i = 0; i < 3; i++) {
+				Common::Array<int> p3 = permute3();
+				persistent->_catacombVariants[0][i] = p3[0];
+				persistent->_catacombVariants[1][i] = p3[1];
+				persistent->_catacombVariants[2][i] = p3[2];
+			}
+			for (int i = 0; i < 3; i++) {
+				Common::Array<int> p3 = permute3();
+				persistent->_catacombPaths[i][0] = (CatacombsPath) p3[0];
+				persistent->_catacombPaths[i][1] = (CatacombsPath) p3[1];
+				persistent->_catacombPaths[i][2] = (CatacombsPath) p3[2];
+			}
+			persistent->_catacombDecoderSkullPosition = (CatacombsPosition) g_vm->getRnd().getRandomNumberRng(0, 2);
+		}
+
+		for (CatacombsPosition i = kCatacombsLeft; i <= kCatacombsRight; i = (CatacombsPosition) (i + 1)) {
+			room->loadHotZones(
+				caVariantGet(i, "Hotspots"), false);
+			room->addStaticLayer(
+				caVariantGet(i, "Background"),
+				i == kCatacombsCenter ? kBackgroundCenterZ : kBackgroundZ);
+		}
+
+		if (persistent->_catacombVariants[level][0] == 2) {
+			room->playAnimLoop("GlowingEyes", 900);
+		}
+
+		room->enableHotzone("LExit");
+		room->enableHotzone("CExit");
+		room->enableHotzone("RExit");
+
+		switch (level) {
+		case 0:
+			room->playSound("IntroMusic");
+			room->enableHotzone(skullHotzones[persistent->_catacombDecoderSkullPosition]);
+			room->selectFrame(
+				caVariantGet(persistent->_catacombDecoderSkullPosition, "SkullDecoder"), 450, 1);
+			for (CatacombsPosition i = kCatacombsLeft; i <= kCatacombsRight; i = (CatacombsPosition) (i + 1)) {
+				room->selectFrame(
+					caVariantGet(i, "SignBoard"), 501, 0);
+				room->selectFrame(
+					caVariantGet(i, signNames[persistent->_catacombPaths[level][i]]), 500, 0);
+			}
+			if (!persistent->isInInventory(kTorch)) {
+				_torchPosition = (CatacombsPosition) g_vm->getRnd().getRandomNumberRng(0, 2);
+				room->enableHotzone(torchHotzones[_torchPosition]);
+				room->playAnimLoop(
+					caVariantGet(_torchPosition, "TorchNormal"),
+					caVariantGet(_torchPosition, "TorchZ").asUint64());
+			}
+			break;
+		case 1:
+			room->enableHotzone("LTorch");
+			room->enableHotzone("CTorch");
+			room->enableHotzone("RTorch");
+			for (CatacombsPosition side = kCatacombsLeft; side <= kCatacombsRight; side = (CatacombsPosition) (side + 1)) {
+				room->selectFrame(
+					caVariantGet(side, "TorchNormalBurst"),
+					caVariantGet(side, "TorchZ").asUint64(), 0);
+			}
+			break;
+		case 2:
+			room->playSound("CollapseSnd", 22009);
+			for (CatacombsPosition side = kCatacombsLeft; side <= kCatacombsRight; side = (CatacombsPosition) (side + 1)) {
+				room->playAnimLoop(
+					caVariantGet(side, "TorchNormal"),
+					caVariantGet(side, "TorchZ").asUint64());
+			}
+			break;
+		}
+
+		g_vm->getHeroBelt()->setColour(HeroBelt::kCool);
+	}
+
+private:
+	void stopTune() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		for (int i = 0; i < 3; i++)
+			room->stopAnim(musicNames[i]);
+	}
+
+	void playTune(CatacombsPosition side) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		stopTune();
+		room->playSoundLoop(musicNames[persistent->_catacombPaths[2][side]]);
+	}
+
+	void renderDecoder() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		room->selectFrame("AnimDecoderScroll", 151, 0);
+		room->selectFrame("AnimDecoderSymbols", 150, _decoderPosition);
+		room->selectFrame(
+			caVariantGet(persistent->_catacombDecoderSkullPosition, "SkullDecoder"), 450, 0);
+		room->enableHotzone("DecoderDone");
+		room->enableHotzone("DecoderDown");
+		room->enableHotzone("DecoderUp");
+	}
+
+	void handleExit(CatacombsPosition side) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		int level = persistent->_catacombLevel;
+
+		if (level == 0 && !_philWarnedTorch && !persistent->isInInventory(kTorch) && persistent->_hintsAreEnabled) {
+			_philWarnedTorch = true;
+			room->playVideo("PhilGrabTheTorch", 0, 22003);
+			return;
+		}
+
+		switch (persistent->_catacombPaths[level][side]) {
+		case kCatacombsHelen:
+			room->disableMouse();
+			if (persistent->_catacombLevel == kCatacombLevelMusic) {
+				persistent->_catacombLevel = kCatacombLevelSign;
+				g_vm->moveToRoom(kPriamRoom);
+			} else {
+				persistent->_catacombLevel = (CatacombsLevel) (persistent->_catacombLevel + 1);
+				g_vm->moveToRoom(kCatacombsRoom);
+			}
+			break;
+		case kCatacombsGuards:
+			room->disableMouse();
+			g_vm->cancelTimer(22007);
+			room->fadeOut(1000, 22016);
+			break;
+		case kCatacombsPainAndPanic:
+			room->disableMouse();
+			g_vm->cancelTimer(22007);
+			persistent->_catacombPainAndPanic = true;
+			g_vm->moveToRoom(kCatacombsRoom);			
+			break;
+		}
+	}
+
+	void lightTorchL1(CatacombsPosition side) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		bool isHelen = persistent->_catacombPaths[1][side] == kCatacombsHelen;
+		room->playAnim(
+			caVariantGet(side, isHelen ? "TorchLongBurst" : "TorchNormalBurst"),
+			caVariantGet(side, "TorchZ").asUint64(),
+			PlayAnimParams::disappear(), kL1TrochLitLeft + side - kCatacombsLeft);
+		room->playSound("SndTorchBurst");
+		room->disableHotzone(torchHotzones[side]);
+	}
+
+	Common::String caVariantGet(CatacombsPosition side, const Common::String &property) {
+		Persistent *persistent = g_vm->getPersistent();
+		int level = persistent->_catacombLevel;
+		int variant = persistent->_catacombVariants[level][side];
+		Common::String ret = _caMapTxt[side].get(variant, property);
+		if (ret == "") {
+			debug("No attrinute for %d/%s", side, property.c_str());
+		}
+		return ret;
+	}
+
+	Common::Array<int> permute3() {
+		Common::Array <int> ret;
+		int x = g_vm->getRnd().getRandomNumberRng(0, 5);
+		int a = x / 2;
+		ret.push_back(a);
+		int cand1 = a == 0 ? 1 : 0;
+		int cand2 = 0;
+		for (cand2 = 0; cand2 == a || cand2 == cand1; cand2++);
+		if (x % 2) {
+			ret.push_back(cand2);
+			ret.push_back(cand1);
+		} else {
+			ret.push_back(cand1);
+			ret.push_back(cand2);
+		}
+		return ret;
+	}
+
+	CatacombsPosition _torchPosition;
+	TextTable _caMapTxt[3];
+	bool _philWarnedTorch;
+	bool _philBangPlayed;
+	int _decoderPosition;
+};
+
+Common::SharedPtr<Hadesch::Handler> makeCatacombsHandler() {
+	return Common::SharedPtr<Hadesch::Handler>(new CatacombsHandler());
+}
+
+}
diff --git a/engines/hadesch/rooms/credits.cpp b/engines/hadesch/rooms/credits.cpp
new file mode 100644
index 0000000000..8f56d148cf
--- /dev/null
+++ b/engines/hadesch/rooms/credits.cpp
@@ -0,0 +1,84 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+
+namespace Hadesch {
+
+enum {
+	kBackgroundZ = 10000,
+	kCreditsZ = 1000
+};
+
+class CreditsHandler : public Handler {
+public:
+	CreditsHandler(bool inOptions) {
+		_inOptions = inOptions;
+	}
+
+	void handleClick(const Common::String &name) override {
+	}
+
+	void handleEvent(int eventId) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+
+		switch(eventId) {
+		case 31001:
+			if (_inOptions)
+				g_vm->enterOptions();
+			else
+				g_vm->moveToRoom(g_vm->getPreviousRoomId());
+			break;
+		}
+	}
+
+	void frameCallback() override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		int timeElapsed = (g_vm->getCurrentTime() - startTime);
+		room->selectFrame("h2030ba0", kCreditsZ, 0,
+				  Common::Point(0, 481
+						- timeElapsed * 6151 / 136000
+					  ));
+	}
+
+	void prepareRoom() override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->disableHeroBelt();
+		room->disableMouse();
+		room->addStaticLayer("h2030pa0", kBackgroundZ);
+		room->playVideo("c2590ma0", 0, 31001);
+		room->selectFrame("h2030ba0", kCreditsZ, 0,
+				  Common::Point(0, 481));
+		startTime = g_vm->getCurrentTime();
+	}
+private:
+	int startTime;
+	bool _inOptions;
+};
+
+Common::SharedPtr<Hadesch::Handler> makeCreditsHandler(bool inOptions) {
+	return Common::SharedPtr<Hadesch::Handler>(new CreditsHandler(inOptions));
+}
+
+}
diff --git a/engines/hadesch/rooms/crete.cpp b/engines/hadesch/rooms/crete.cpp
new file mode 100644
index 0000000000..c9ad845da8
--- /dev/null
+++ b/engines/hadesch/rooms/crete.cpp
@@ -0,0 +1,1608 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+#include "hadesch/ambient.h"
+
+namespace Hadesch {
+
+static const char *kTalusImage = "r1100bb0";
+static const char *kTalusImageWithShip = "r1100bc0";  
+static const char *kTalusMovie = "r1100ba0";
+static const char *kTalusHotzone = "Talus";
+static const char *kTavernImage = "r2190ba0";
+static const char *kTavernHotzone = "Tavern";
+static const char *kPoseidonHighlight = "r1230ba0";
+static const char *kZeusHighlight = "r1240ba0";
+static const char *kHermesHighlight = "r2320ba0";
+static const char *kHornless1 = "g0170ob0";
+static const char *kHornless2 = "g0170oe0";
+static const char *kHornless3 = "g0170oh0";
+static const char *kHornless4 = "g0170ok0";
+static const char *kHorned = "g1800ob0";
+static const char *kHornedHotzone = "HornedStatue";
+static const char *kHornless1Hotzone = "HornlessStatue1";
+static const char *kHornless2Hotzone = "HornlessStatue2";
+static const char *kHornless3Hotzone = "HornlessStatue3";
+static const char *kHornless4Hotzone = "HornlessStatue4";
+static const char *kMerchantAnim = "r2130ba0";
+static const char *kOneManBandAnim = "r2040ba1";
+static const char *kOneManBandHotZone = "OneManBand";
+static const char *kAtlantisOpening = "r1210bb0";
+static const char *kAtlantisLargeDisk = "r1210be0";
+static const char *kAtlantisMediumDisk = "r1210bf0";
+static const char *kAtlantisSmallDisk = "r1210bg0";
+static const char *kAtlantisDiskBackground = "r1210os0";
+
+static const int vaseSol[] = {2, 3, 2, 3};
+static const char *vaseSound[] = {
+	"r1220ec0",
+	"r1220ed0",
+	"r1220ee0",
+	"r1220ef0"
+};
+
+static const char *vaseSegment[] = {
+	"r1220bc0",
+	"r1220bd0",
+	"r1220be0",
+	"r1220bf0"	
+};
+
+enum {
+	kBackgroundZ = 10000,
+	kTalusZ = 6000,
+	kTavernImageZ = 5000,
+	kOneManBandZ = 1600,
+	kMerchantStandZ = 1205,
+	kMerchantZ = 1200,
+	kAtlantisDiskBackgroundZ = 1100,
+	kAtlantisLargeDiskZ = 1000,
+	kAtlantisMediumDiskZ = 900,
+	kAtlantisSmallDiskZ = 800,
+	kAtlantisOpeningZ = 700
+};
+
+enum {
+	kIntroMerchantVideoFinished = 12107,
+	kTakenHorned = 12115,
+	kTakenHornless = 12118,
+	kMerchantIdleTimer = 12121,
+	kMerchantIdleAnimCleanup = 12122,
+	kTalueMovieCompleted = 12129,
+	// 12137 is the end of statue animation that we handle as functor instead
+	kAtlantisDoorOpens = 12307,
+	kTavernMovieCompleted = 1012001,
+	kIntroMerchantPanFinished = 1012002,
+	kAtlantisBoatIntro2Finished = 1012003,
+	kTakenWood = 1012005,
+	kSoundVaseSegment1Finished = 1012006,
+	kSoundVaseSegment2Finished = 1012007,
+	kSoundVaseSegment3Finished = 1012008,
+	kSoundVaseSegment4Finished = 1012009,
+	kCoinGiven = 1012010,
+	kSandalsPlaced = 1012011
+};
+
+static const Common::Point strongBoxSideDotOffsets[] = {
+	Common::Point(1060, 278),
+	Common::Point(1090, 310),
+	Common::Point(1060, 339),
+	Common::Point(1032, 305)
+};
+
+static const Common::Point strongBoxTileOffsets[] = {
+	Common::Point(1005, 274),
+	Common::Point(1035, 276),
+	Common::Point(1064, 278),
+	Common::Point(1093, 280),
+	Common::Point(1005, 304),
+	Common::Point(1035, 306),
+	Common::Point(1064, 308),
+	Common::Point(1093, 310),
+	Common::Point(1035, 246),
+	Common::Point(1064, 248),
+	Common::Point(1035, 337),
+	Common::Point(1064, 339)
+};
+
+static const int baseFrame[4][8] = {
+	{  1,  3,  4,  6,  1,  3,  4,  6 },
+	{  7,  9, 10, 12, 13, 15, 16, 18 },
+	{ 19, 21, 22, 24, 25, 27, 28, 30 },
+	{ 31, 33, 34, 36, 31, 33, 34, 36 }
+};
+
+static const int kTileMovementTime = 500;
+
+class StrongBoxTile {
+public:
+	enum Letter {
+		kLetterZ = 1,
+		kLetterE,
+		kLetterU,
+		kLetterS
+	};
+	enum Orientation {
+		kOrientation0 = 0,
+		kOrientation90 = 90,
+		kOrientation180 = 180,
+		kOrientation270 = 270
+	};
+
+	void rotate() {
+		_orientation = (Orientation) ((_orientation + 90) % 360);
+	}
+
+	void show() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		int orientation = _orientation / 45;
+		if (_nextPosition != -1 && g_vm->getCurrentTime() > kTileMovementTime + _movementStartTime) {
+			_position = _nextPosition;
+			_nextPosition = -1;
+		}
+		Common::Point pos = strongBoxTileOffsets[_position];
+		if (_nextPosition != -1) {
+			double frac = (g_vm->getCurrentTime() - _movementStartTime + 0.0) / kTileMovementTime;
+			pos = strongBoxTileOffsets[_position] * (1-frac) + strongBoxTileOffsets[_nextPosition] * frac;
+		}
+		int zVal = 500;
+		switch (_rotationPhase) {
+		case 0:
+		case 2:
+			pos += Common::Point(-5, 4);
+			zVal = 300;
+			break;
+		case 1:
+			orientation++;
+			switch (_position) {
+			case 1:
+				pos = Common::Point(1046, 277);
+				break;
+			case 2:
+				pos = Common::Point(1065, 298);
+				break;
+			case 5:
+				pos = Common::Point(1025, 297);
+				break;
+			case 6:
+				pos = Common::Point(1045, 318);
+				break;
+			}
+			zVal = 300;
+			break;
+		}
+		int frame = baseFrame[_letter - kLetterZ][orientation] - 1;
+		if (_position < 4 && _nextPosition == -1 && _rotationPhase < 0)
+			frame++;
+		room->selectFrame(LayerId("r2010om0", _position, "pos"),
+				  zVal, frame, pos);
+	}
+
+	void setRotationPhase(int phase) {
+		_rotationPhase = phase;
+	}
+
+	bool isMoving() {
+		return _nextPosition != -1 || _rotationPhase != -1;
+	}
+
+	int getPosition() {
+		return _position;
+	}
+
+	Letter getLetter() {
+		return _letter;
+	}
+
+	Orientation getOrientation() {
+		return _orientation;
+	}
+
+	void setPosition(int position) {
+		_position = position;
+	}
+
+	void moveTo(int pos) {
+		_nextPosition = pos;
+		_movementStartTime = g_vm->getCurrentTime();
+	}
+
+	StrongBoxTile() {
+		_letter = kLetterZ;
+		_position = 0;
+		_orientation = kOrientation0;
+		_nextPosition = -1;
+		_rotationPhase = -1;
+	}
+
+	StrongBoxTile(Letter letter, Orientation orientation, int position) {
+		_letter = letter;
+		_position = position;
+		_orientation = orientation;
+		_nextPosition = -1;
+		_rotationPhase = -1;
+	}
+private:
+	Letter _letter;
+	int _position;
+	int _nextPosition;
+	int _movementStartTime;
+	int _rotationPhase;
+	Orientation _orientation;
+};
+
+static const struct {
+	StrongBoxTile::Letter letter;
+	StrongBoxTile::Orientation orientation;
+} initialLetters[12] = {
+	{StrongBoxTile::kLetterZ, StrongBoxTile::kOrientation90},
+	{StrongBoxTile::kLetterE, StrongBoxTile::kOrientation90},
+	{StrongBoxTile::kLetterU, StrongBoxTile::kOrientation90},
+	{StrongBoxTile::kLetterS, StrongBoxTile::kOrientation90},
+	{StrongBoxTile::kLetterE, StrongBoxTile::kOrientation0},
+	{StrongBoxTile::kLetterZ, StrongBoxTile::kOrientation0},
+	{StrongBoxTile::kLetterU, StrongBoxTile::kOrientation270},
+	{StrongBoxTile::kLetterS, StrongBoxTile::kOrientation90},
+	{StrongBoxTile::kLetterU, StrongBoxTile::kOrientation90},
+	{StrongBoxTile::kLetterE, StrongBoxTile::kOrientation90},
+	{StrongBoxTile::kLetterZ, StrongBoxTile::kOrientation0},
+	{StrongBoxTile::kLetterS, StrongBoxTile::kOrientation0}
+};
+
+class CreteHandler : public Handler {
+public:
+	CreteHandler() {
+		_tavernCounter = 0;
+		_oneManBandCounter = 0;
+		_merchantIsBusy = false;
+		memset(_vaseBusy, 0, sizeof(_vaseBusy));
+		for (int i =0; i < 12; i++)
+			_strongBoxTiles[i] = StrongBoxTile(initialLetters[i].letter, initialLetters[i].orientation, i);
+		_strongBoxPopup = false;
+		_fadingHades = false;
+		_fadingHadesStartTime = 0;
+	}
+
+	void handleClick(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		if (name == kTalusHotzone) {
+			room->playVideo(kTalusMovie, kTalusZ, kTalueMovieCompleted,
+					Common::Point(54, 29));
+			room->setLayerEnabled(kTalusImageWithShip, false);
+			return;
+		}
+
+		if (name == "MinosPalace") {
+			room->disableMouse();
+			g_vm->moveToRoom(kMinosPalaceRoom);
+			return;
+		}
+
+		if (name == "Poseidon") {
+			Common::Array<Common::String> videos;
+			videos.push_back("r1230na0");
+			videos.push_back("r1230nb0");
+			videos.push_back("r1230nc0");
+
+			room->playStatueSMK(kPoseidonStatue,
+					    kPoseidonHighlight,
+					    5000,
+					    videos, 16, 39);
+			return;
+		}
+
+		if (name == "Zeus") {
+			Common::Array<Common::String> videos;
+			videos.push_back("r1240wa0");
+			videos.push_back("r1240wb0");
+
+			room->playStatueSMK(kZeusStatue, kZeusHighlight, 5000,
+					    videos, 23, 35);
+			return;
+		}
+
+		if (name == "Hermes") {
+			Common::Array<Common::String> videos;
+			videos.push_back("r2320na0");
+			videos.push_back("r2320nb0");
+
+			room->playStatueSMK(kHermesStatue,
+					    kHermesHighlight,
+					    4000,
+					    videos, 22, 39, kOffsetRightRoom);
+			return;
+		}
+
+		if (name == kTavernHotzone) {
+			room->playAnimLoop(kTavernImage, 5000, kOffsetRightRoom);
+			room->playVideo(_tavernTalks[_tavernCounter], 5000,
+					   kTavernMovieCompleted,
+					   kOffsetRightRoom);
+			_tavernCounter = (_tavernCounter + 1) % _tavernTalks.size();
+			return;
+		}
+
+		if (name == "Argo") {
+			g_vm->moveToRoom(kArgoRoom);
+			return;
+		}
+
+		if (name == kHornedHotzone) {
+			g_vm->getHeroBelt()->placeToInventory(kHornedStatue, kTakenHornless);
+			room->setLayerEnabled(kHorned, false);
+			persistent->_creteShowHorned = false;
+			_merchantIsBusy = true;
+			room->disableHotzone(kHornedHotzone);
+			room->disableMouse();
+			return;
+		}
+
+		if (name == kHornless1Hotzone) {
+			g_vm->getHeroBelt()->placeToInventory(kHornlessStatue1, kTakenHorned);
+			room->setLayerEnabled(kHornless1, false);
+			persistent->_creteShowHornless1 = false;
+			room->disableHotzone(kHornless1Hotzone);
+			_merchantIsBusy = true;
+			room->disableMouse();
+			return;
+		}
+
+		if (name == kHornless2Hotzone) {
+			g_vm->getHeroBelt()->placeToInventory(kHornlessStatue2, kTakenHorned);
+			room->setLayerEnabled(kHornless2, false);
+			persistent->_creteShowHornless2 = false;
+			room->disableHotzone(kHornless2Hotzone);
+			_merchantIsBusy = true;
+			room->disableMouse();
+			return;
+		}
+
+		if (name == kHornless3Hotzone) {
+			g_vm->getHeroBelt()->placeToInventory(kHornlessStatue3, kTakenHorned);
+			room->setLayerEnabled(kHornless3, false);
+			persistent->_creteShowHornless3 = false;
+			room->disableHotzone(kHornless3Hotzone);
+			_merchantIsBusy = true;
+			room->disableMouse();
+			return;
+		}
+
+		if (name == kHornless4Hotzone) {
+			g_vm->getHeroBelt()->placeToInventory(kHornlessStatue4, kTakenHorned);
+			room->setLayerEnabled(kHornless4, false);
+			persistent->_creteShowHornless4 = false;
+			room->disableHotzone(kHornless4Hotzone);
+			_merchantIsBusy = true;
+			room->disableMouse();
+			return;
+		}
+
+		if (name == kOneManBandHotZone) {
+			room->playVideo(
+				Common::String::format(
+					"r2040b%c0", 'a' + (_oneManBandCounter % 3)),
+				1600,
+				12135, Common::Point(730, 183));
+			room->stopAnim(kOneManBandAnim);
+			_oneManBandCounter++;
+			return;
+		}
+
+		if (name == "AtlantisBoat") {
+			room->pushHotZones("Door.HOT");
+			room->disableHotzone("wood");
+			room->selectFrame("r1010ob0", 1200, 0);
+			room->selectFrame(kAtlantisDiskBackground, kAtlantisDiskBackgroundZ, 0);
+			for (unsigned i = 0; i < 3; i++)
+				_atlantisBoatPosition[i] = g_vm->getRnd().getRandomNumberRng(1, 7);
+			renderAtlantisDisks();
+			if (persistent->_creteIntroAtlantisWood) {
+				room->disableMouse();
+				room->playVideo("R1210BA0", 600, kAtlantisBoatIntro2Finished);
+				persistent->_creteIntroAtlantisWood = false;
+			}
+			return;
+				
+		}
+
+		if (name == "SmallDisk") {
+			advanceAtlantisDisk(2);
+			return;
+		}
+
+		if (name == "MediumDisk") {
+			advanceAtlantisDisk(1);
+			return;
+		}
+
+		if (name == "LargeDisk") {
+			advanceAtlantisDisk(0);
+			return;
+		}
+
+		if (name == "wood") {
+			room->selectFrame(kAtlantisOpening, kAtlantisOpeningZ, 9);
+			g_vm->getHeroBelt()->placeToInventory(kWood, kTakenWood);
+			room->popHotZones();
+			room->disableHotzone("AtlantisBoat");
+			persistent->_creteShowAtlantisBoat = false;
+			return;
+		}
+
+		if (name == "Background") {
+			room->popHotZones();
+			room->stopAnim("r1010ob0");
+			hideStrongBox();
+			hideAtlantisPopupOverlays();
+			showMiniStrongBox();
+			return;
+		}
+
+		for (int i = 0; i < 4; i++) {
+			if (name == Common::String::format("VaseSegment%d", i + 1) && !_vaseBusy[i]) {
+				_vasePos[i]++;
+				if (i == 1 || i == 3)
+					_vasePos[i] %= 6;
+				else
+					_vasePos[i] %= 4;
+				_vaseBusy[i] = true;
+				renderVase();
+				room->playSound(vaseSound[i], kSoundVaseSegment1Finished + i);
+				return;
+			}
+		}
+
+		if (name == "sandals") {
+			g_vm->getHeroBelt()->placeToInventory(kSandals, kSandalsPlaced);
+			room->selectFrame("r1220ba0", 500, 0);
+			room->disableMouse();
+			persistent->_creteSandalsState = Persistent::SANDALS_TAKEN;
+			return;
+		}
+
+		if (name == "AlchemistStand") {
+			_alchemistAmbient.play(false);
+			return;
+		}
+
+		if (name == "StrongBoxClosed"
+			|| name == "StrongBoxOpen"
+			|| name == "StrongBoxOpenPotion"
+			|| name == "StrongBoxOpenNoPotion") {
+			showStrongBox();
+			return;
+		}
+
+		if (name == "Latch") {
+			room->disableMouse();
+			persistent->_creteStrongBoxState = Persistent::BOX_OPEN;
+			room->playAnimWithSound("r2230bb0", "g0082ea0", 1000,
+						PlayAnimParams::keepLastFrame().partial(1, 3),
+						12402, kOffsetRightRoom);
+			return;
+		}
+
+		if (name == "ButtonN" && !strongBoxIsBusy()) {
+			strongBoxMoveTiles(1, 8, 9, 2);
+			return;
+		}
+
+		if (name == "ButtonE" && !strongBoxIsBusy()) {
+			strongBoxMoveTiles(2, 3, 7, 6);
+			return;
+		}
+
+		if (name == "ButtonW" && !strongBoxIsBusy()) {
+			strongBoxMoveTiles(1, 5, 4, 0);
+			return;
+		}
+
+		if (name == "ButtonS" && !strongBoxIsBusy()) {
+			strongBoxMoveTiles(5, 6, 11, 10);
+			return;
+		}
+
+		if (name == "ButtonC" && !strongBoxIsBusy()) {
+			g_vm->addTimer(12409, 250);
+			room->playSound("r2230ed0", 12412);
+			for (int i = 0; i < 12; i++) {
+				int pos = _strongBoxTiles[i].getPosition();
+				if (pos == 1 || pos == 2 || pos == 5 || pos == 6) {
+					_strongBoxTiles[i].setRotationPhase(0);
+				}
+			}
+			return;
+		}
+
+		if (name == "Potion") {
+			room->disableHotzone("Potion");
+			g_vm->getHeroBelt()->placeToInventory(kCoin, kCoinGiven);
+			persistent->_creteStrongBoxState = Persistent::BOX_OPEN_NO_POTION;
+			room->selectFrame("r2230bf0", 300, -1, kOffsetRightRoom);
+			return;
+		}
+		/*
+TODO:
+ MNSH: Merchant
+*/
+	}
+
+	void handleEvent(int eventId) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		switch (eventId) {
+		case kTakenWood:
+			room->stopAnim(kAtlantisOpening);
+			room->stopAnim("r1010ob0");
+			room->stopAnim(kAtlantisDiskBackground);
+			room->enableMouse();
+			break;
+		case kTalueMovieCompleted:
+			room->setLayerEnabled(kTalusImageWithShip, true);
+			break;
+		case kTavernMovieCompleted:
+			room->selectFrame(kTavernImage, kTavernImageZ, 0, kOffsetRightRoom);
+			break;
+		case kMerchantIdleTimer:
+			if (!persistent->_creteShowMerchant
+			    || !persistent->_creteShowHornless4 || _merchantIsBusy)
+				break;
+			room->setLayerEnabled(kHornless4, false);
+			room->disableHotzone(kHornless4Hotzone);
+			room->playAnim(kMerchantAnim, kMerchantZ,
+				       PlayAnimParams::disappear().partial(1, -2),
+				       kMerchantIdleAnimCleanup);
+			break;
+		case kMerchantIdleAnimCleanup:
+			room->selectFrame(kMerchantAnim, kMerchantZ, 0);
+			room->setLayerEnabled(kHornless4, persistent->_creteShowHornless4);
+			room->setHotzoneEnabled(kHornless4Hotzone, persistent->_creteShowHornless4 && persistent->_creteTriedHornless[2]);
+			break;
+		case kIntroMerchantPanFinished:
+			room->setLayerEnabled(kMerchantAnim, false);
+			room->playVideo("R2200BA0", kMerchantZ,
+					kIntroMerchantVideoFinished,
+					Common::Point(308, 99));
+			break;
+		// TODO: replay of R2200BB0 and R2200BC0
+		case kIntroMerchantVideoFinished:
+			room->disableMouse();
+			room->playVideo("R2200BB0", kMerchantZ,
+					12108,
+					Common::Point(304, 113));
+			_merchantIsBusy = true;
+			break;
+		case 12108:
+			room->disableMouse();
+			room->playVideo("R2200BC0", kMerchantZ,
+					12109,
+					Common::Point(304, 110));
+			_merchantIsBusy = true;
+			break;
+		case 12109:
+			room->selectFrame(kMerchantAnim, kMerchantZ, 0);
+			room->enableMouse();
+			_merchantIsBusy = false;
+			break;
+		case kTakenHorned:
+			room->setLayerEnabled(kMerchantAnim, false);
+			room->playVideo("r2210ba0", kMerchantZ, 12117,
+					Common::Point(344, 111));
+			room->disableMouse();
+			break;
+		case 12117:
+			room->playVideo("r2210bb0", kMerchantZ, 12119,
+				Common::Point(308, 112));
+			break;
+		case kTakenHornless:
+			room->setLayerEnabled(kMerchantAnim, false);
+			room->playVideo("r2240ba0", kMerchantZ, 12119,
+				Common::Point(314, 91));
+			_merchantIsBusy = true;
+			break;
+		case 12119:
+			room->selectFrame(kMerchantAnim, kMerchantZ, 0);
+			if (!persistent->_cretePlayedEyeGhostTown) {
+				room->playVideo("r2210bc0", 1000, 12120,
+						   Common::Point(0, 216));
+				persistent->_cretePlayedEyeGhostTown = true;
+			} else {
+				room->enableMouse();
+				_merchantIsBusy = false;
+			}
+			break;
+		case 12120:
+			room->selectFrame(kMerchantAnim, kMerchantZ, 0);
+			room->enableMouse();
+			_merchantIsBusy = false;
+			break;
+		case 12128:
+		case 12134:
+		case 12143:
+		case 12146:
+			room->enableMouse();
+			break;
+		case 12135:
+			room->playAnimLoop(kOneManBandAnim, kOneManBandZ, kOffsetRightRoom);
+			break;
+		case 12142:
+			if (persistent->_quest == kMedusaQuest && !persistent->_cretePlayedPhilAlchemist) {
+				persistent->_cretePlayedPhilAlchemist = true;
+				room->playVideo("r2220bc0", 1000, 12143, Common::Point(640, 216));
+				room->disableMouse();
+			}
+			if (persistent->_quest == kRescuePhilQuest && !persistent->_cretePlayedZeusCheckOutThatBox && persistent->_hintsAreEnabled) {
+				g_vm->addTimer(12144, 5000, -1);
+			}
+			break;
+		case 12144:
+			if (!room->isMouseEnabled() || persistent->_cretePlayedZeusCheckOutThatBox)
+				break;
+			persistent->_cretePlayedZeusCheckOutThatBox = true;
+			room->disableMouse();
+			room->playAnimWithSound("r2230ba0", "r2230wa0", 4000, PlayAnimParams::keepLastFrame(), 12145,
+						kOffsetRightRoom);
+			break;
+		case kAtlantisBoatIntro2Finished:
+			room->enableMouse();
+			break;
+		case kAtlantisDoorOpens:
+			room->disableHotzone("LargeDisk");
+			room->disableHotzone("MediumDisk");
+			room->disableHotzone("SmallDisk");
+			room->disableHotzone("Background");
+			room->enableHotzone("wood");
+			room->playVideo("r1210bd0", 600, 12308);
+			break;
+		case 12308:
+			room->enableMouse();
+			break;
+		case kSoundVaseSegment1Finished:
+		case kSoundVaseSegment2Finished:
+		case kSoundVaseSegment3Finished:
+		case kSoundVaseSegment4Finished: {
+			int vase = eventId - kSoundVaseSegment1Finished;
+			bool won = true;
+			_vaseBusy[vase] = false;
+			for (int i = 0; i < 4; i++) {
+				if (_vaseBusy[i])
+					won = false;
+			}
+
+			for (int i = 0; i < 4; i++) {
+				if (_vasePos[i] != vaseSol[i])
+					won = false;
+			}
+
+			if (!won)
+				break;
+
+			for (int i = 0; i < 4; i++)
+				room->stopAnim(vaseSegment[i]);
+
+			for (int i = 0; i < 4; i++)
+				room->disableHotzone(Common::String::format("VaseSegment%d", i + 1));
+
+			room->playAnim("r1220ba0", 500,
+				       PlayAnimParams::disappear().partial(0, 17), 12206);
+			room->playVideo("r1220mb0", 0);
+			room->playSound("r1220ea0");
+			room->disableMouse();
+			break;
+		}
+		case 12206:
+			showSandals();
+			persistent->_creteSandalsState = Persistent::SANDALS_SOLVED;
+			room->enableMouse();
+			break;
+		case 12303:
+		case 12304:
+		case 12305:
+			if (_atlantisBoatPosition[0] == 0
+			    && _atlantisBoatPosition[1] == 0
+			    && _atlantisBoatPosition[2] == 0) {
+				handleEvent(12306);
+			}
+			break;
+		case 12306:
+			room->disableMouse();
+			hideAtlantisPopupOverlays();
+			room->playAnimWithSound(kAtlantisOpening, "r1210eb0",kAtlantisOpeningZ,
+						PlayAnimParams::keepLastFrame().partial(0, 8),
+						kAtlantisDoorOpens);
+			break;
+		case 12402:
+			room->enableMouse();
+			redrawStrongBox();
+			// Fallthrough
+		case 12403:
+			room->disableMouse();
+			room->playAnimWithSound("r2230ba0", "r2230wb0", 4000,
+						PlayAnimParams::keepLastFrame(), 12404,
+						kOffsetRightRoom);
+			break;
+		case 12404:
+		case 12405:
+			if (persistent->_hintsAreEnabled)
+				g_vm->addTimer(12406, 5000);
+			// Fallthrough
+		case 12407:
+		case 12408:
+		case 12145:
+			room->playAnim("r2230ba0", 4000, PlayAnimParams::disappear().backwards(), -1,
+				       kOffsetRightRoom);
+			room->enableMouse();
+			break;
+		case 12406:
+			if (!room->isMouseEnabled())
+				break;
+			room->disableMouse();
+			room->playAnimWithSound("r2230ba0", "r2230wc0", 4000,
+						PlayAnimParams::keepLastFrame(), 12407,
+						kOffsetRightRoom);
+			break;
+		case 12409:
+			for (int i = 0; i < 12; i++) {
+				int pos = _strongBoxTiles[i].getPosition();
+				if (pos == 1 || pos == 2 || pos == 5 || pos == 6) {
+					_strongBoxTiles[i].setRotationPhase(1);
+				}
+			}
+			g_vm->addTimer(12410, 250);
+			break;
+		case 12410:
+			for (int i = 0; i < 12; i++) {
+				int pos = _strongBoxTiles[i].getPosition();
+				if (pos == 1 || pos == 2 || pos == 5 || pos == 6) {
+					_strongBoxTiles[i].setRotationPhase(2);
+					_strongBoxTiles[i].rotate();
+					switch (pos) {
+					case 1:
+						_strongBoxTiles[i].setPosition(2);
+						break;
+					case 2:
+						_strongBoxTiles[i].setPosition(6);
+						break;
+					case 5:
+						_strongBoxTiles[i].setPosition(1);
+						break;
+					case 6:
+						_strongBoxTiles[i].setPosition(5);
+						break;
+					}
+				}
+			}
+			g_vm->addTimer(12411, 250);
+			break;
+		case 12411:
+			for (int i = 0; i < 12; i++) {
+				_strongBoxTiles[i].setRotationPhase(-1);
+			}
+			redrawStrongBox();
+			break;
+		case 12414:
+			room->playVideo("v4190ma0", 0, 12415);
+			break;
+		case 12204:
+		case 12415:
+			room->enableMouse();
+			break;
+		case kCoinGiven:
+			g_vm->getHeroBelt()->placeToInventory(kPotion);
+			break;
+		case 12124:
+			_fadingHades = true;
+			_fadingHadesStartTime = g_vm->getCurrentTime();
+			break;
+		case 12125:
+			room->stopAnim("r2035ba0");
+			room->playVideo("R2035BE0", 1200, 12127);
+			break;
+		case 12127:
+			g_vm->moveToRoom(kDaedalusRoom);
+			break;
+		case kSandalsPlaced:
+			room->playVideo("r1220bb0", 0, 12204);
+			break;
+		}
+	}
+
+	void prepareRoom() override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		Quest quest = persistent->_quest;
+
+		if (persistent->_creteHadesPusnishesPainAndPanic) {
+			room->disableHeroBelt();
+			room->disableMouse();
+			room->addStaticLayer("r2035pa0", kBackgroundZ); // background
+			room->selectFrame("r2035ba0", 1200, 0);
+
+			// Originally event 12123
+			room->playSound("r2035wa0", 12124);
+			g_vm->moveToRoom(kDaedalusRoom);
+			return;
+		}
+		
+		room->loadHotZones("Crete.HOT", false);
+		room->addStaticLayer("r1010pa0", kBackgroundZ); // background
+		g_vm->getHeroBelt()->setColour(HeroBelt::kWarm);
+
+		if (quest != kMedusaQuest && quest != kRescuePhilQuest) {
+			room->playAnimLoop("r2010oe0", 7000, kOffsetRightRoom);
+			room->playAnimLoop("r2010tb0", 6500, kOffsetRightRoom);
+		}
+		room->playAnimLoop("r2010ta0", 7600, kOffsetRightRoom);
+		room->playAnimLoop("r1120ba0", 5500);
+		room->playAnimLoop("r1160ba0", 5000);
+		room->playAnimLoop("r1170ba0", 5000);
+		
+		room->selectFrame(kTavernImage, kTavernImageZ, 0, kOffsetRightRoom);
+		room->enableMouse();
+		room->setPannable(true);
+		room->enableHotzone(kTavernHotzone);
+		room->enableHotzone("Argo");
+		room->enableHotzone("Hermes");
+		room->enableHotzone("Zeus");
+		room->enableHotzone("Poseidon");
+		room->enableHotzone("MinosPalace");
+		if (quest != kMedusaQuest && quest != kRescuePhilQuest) {
+			room->enableHotzone("AlchemistStand");
+		}
+
+		room->setUserPanCallback(-1, -1, 12140, 12142);
+		
+		if (quest == kMedusaQuest && !persistent->_creteAlchemistExploded) {
+			persistent->_creteAlchemistExploded = true;
+			room->disableMouse();
+			room->playAnimWithSound("r1190ba0", "r1190ea0", 1005, PlayAnimParams::disappear(), 12128);
+		}
+
+		if (quest == kRescuePhilQuest || quest == kMedusaQuest) {
+			room->selectFrame("r2010op0", 7500, 0, kOffsetRightRoom);
+		}
+
+		if (quest == kMedusaQuest) {
+			room->playAnimWithSound("r2220bb0", "r2220eb0", 4500, PlayAnimParams::loop(), -1, kOffsetRightRoom);
+		}
+
+		showMiniStrongBox();
+
+		switch (quest) {
+		case kCreteQuest:
+			_tavernTalks.push_back("r2250xa0");
+			_tavernTalks.push_back("r2250xb0");
+			_tavernTalks.push_back("r2250xc0");
+			_tavernTalks.push_back("r2250xd0");
+			_tavernTalks.push_back("r2250xe0");
+			_tavernTalks.push_back("r2250xf0");
+			_tavernTalks.push_back("r2220xa0");
+			_tavernTalks.push_back("r2220xb0");
+			_tavernTalks.push_back("r2220xc0");
+			_tavernTalks.push_back("r2220xd0");
+			_tavernTalks.push_back("r2220xe0");
+			_tavernTalks.push_back("r2220xf0");
+			break;
+		case kTroyQuest:
+			_tavernTalks.push_back("r2260xa0");
+			_tavernTalks.push_back("r2260xb0");
+			_tavernTalks.push_back("r2195xa0");
+			_tavernTalks.push_back("r2195xb0");
+			_tavernTalks.push_back("r2290xf0");
+			_tavernTalks.push_back("r2220xa0");
+			_tavernTalks.push_back("r2220xb0");
+			_tavernTalks.push_back("r2220xc0");
+			_tavernTalks.push_back("r2220xd0");
+			_tavernTalks.push_back("r2220xe0");
+			_tavernTalks.push_back("r2220xf0");
+			break;
+		case kMedusaQuest:
+			_tavernTalks.push_back("r2270xa0");
+			_tavernTalks.push_back("r2270xb0");
+			_tavernTalks.push_back("r2290xg0");
+			_tavernTalks.push_back("r2290xh0");
+			_tavernTalks.push_back("r2290xi0");
+			_tavernTalks.push_back("r2290xj0");
+			_tavernTalks.push_back("r2290xk0");
+			_tavernTalks.push_back("r2290xl0");
+			break;
+		case kRescuePhilQuest:
+			_tavernTalks.push_back("r2280xa0");
+			_tavernTalks.push_back("r2280xc0");
+			_tavernTalks.push_back("r2280xd0");
+			_tavernTalks.push_back("r2290xg0");
+			_tavernTalks.push_back("r2290xh0");
+			_tavernTalks.push_back("r2290xi0");
+			_tavernTalks.push_back("r2290xj0");
+			_tavernTalks.push_back("r2290xk0");
+			_tavernTalks.push_back("r2290xl0");
+			break;
+
+		// To silence warning
+		case kNoQuest:
+		case kEndGame:
+		case kNumQuests:
+			break;
+		}
+		_tavernTalks.push_back("r2290xa0");
+		_tavernTalks.push_back("r2290xb0");
+		_tavernTalks.push_back("r2290xc0");
+		_tavernTalks.push_back("r2290xd0");
+		_tavernTalks.push_back("r2290xe0");
+
+		int bg1_variant = -1;
+		int bg2_variant = -1;
+
+		bool showOiBoat = false;
+
+		if (!persistent->_creteShowMerchant && (quest != kMedusaQuest || !persistent->_medisleShowFates)) {
+			bg1_variant = g_vm->getRnd().getRandomNumberRng(0, 2);
+			bg2_variant = g_vm->getRnd().getRandomNumberRng(0, 2);
+			debug("BG variants %d and %d", bg1_variant, bg2_variant);
+		}
+
+		if (randomBool()) {
+			showOiBoat = true;
+			room->addStaticLayer("r1010oi0", 4000);
+		}
+		if (randomBool())
+			room->addStaticLayer("r1010oj0", 4000);
+		if (randomBool())
+			room->addStaticLayer("r1010ok0", 4000);
+		if (randomBool())
+			room->addStaticLayer("r1010ol0", 4000);
+		if (randomBool())
+			room->addStaticLayer("r1010on0", 4001);
+		if (randomBool())
+			room->addStaticLayer("r1010oo0", 4000);
+		if (randomBool())
+			room->addStaticLayer("r1010op0", 4100);
+		if (randomBool())
+			room->addStaticLayer("r1010or0", 6500);
+
+		if (quest != kCreteQuest)
+			persistent->_creteShowAtlantisBoat = false;
+
+		if (persistent->_creteShowAtlantisBoat) {
+			room->addStaticLayer("r1010od0", 3900);
+			room->addStaticLayer("r1010ta0", 3800);
+			room->enableHotzone("AtlantisBoat");
+		}
+
+		room->playAnimLoop("r1010om0", 3000);
+
+		int minotaurPosition = -1;
+		int toughGuyPosition = -1;
+		int womanSmellPosition = -1;
+		if (quest == kCreteQuest && !persistent->_creteShowMerchant && !persistent->_creteShowAtlantisBoat)
+			minotaurPosition = g_vm->getRnd().getRandomNumberRng(0, 3);
+
+		bool showCat = false;
+		bool showStatueMan = false;
+		bool showOldMan = false;
+		bool showBlondBoy = false;
+		bool showMotherAndKidSmall = false;
+		bool showMotherAndKid = false;
+		bool showManSelling = false;
+		bool showDrawinWithoutWoman = false;
+		bool showWomanDrawing = false;
+		bool showWomanGraffiti = false;
+		bool showGirlAndNut = false;
+		bool showOneManBand = false;
+		int ambientComposite = g_vm->getRnd().getRandomNumberRng(1, 8);
+		debug("Ambients from Composite %d selected. \n", ambientComposite);
+		switch (ambientComposite) {
+		case 1:
+			showGirlAndNut = true;
+			if (quest != kCreteQuest && quest != kTroyQuest) {
+				toughGuyPosition = 1;
+				break;
+			}
+			if (minotaurPosition == 1) {
+				if (randomBool())
+					toughGuyPosition = 0;
+				break;
+
+			}
+			if (persistent->_creteShowMerchant) {
+				showOneManBand = true;
+				showWomanGraffiti = true;
+				break;
+			}
+			if (randomBool()) {
+				toughGuyPosition = 0;
+				showOneManBand = true;
+				showWomanGraffiti = true;
+			}
+			else
+				toughGuyPosition = 1;
+			break;
+		case 2:
+			womanSmellPosition = g_vm->getRnd().getRandomNumberRng(0, 2);
+			showWomanDrawing = true;
+			showManSelling = true;
+			if (quest != kMedusaQuest && quest != kRescuePhilQuest)
+				showMotherAndKid = true;
+			showBlondBoy = true;
+			break;
+		case 3:
+			if (quest != kRescuePhilQuest && bg1_variant != 0 && bg1_variant != 1)
+				showCat = true;
+			showOldMan = true;
+			if (!persistent->_creteShowMerchant) {
+				showManSelling = true;
+				toughGuyPosition = 2;
+			}
+			showWomanGraffiti = true;
+			showWomanDrawing = true;
+			break;
+		case 4:
+			if (quest != kRescuePhilQuest && bg1_variant != 0 && bg1_variant != 1)
+				showCat = true;
+			showWomanDrawing = true;
+			toughGuyPosition = 2;
+			showOneManBand = true;
+			break;
+		case 5:
+			if (quest != kCreteQuest && quest != kTroyQuest)
+				toughGuyPosition = 1;
+			else
+				toughGuyPosition = g_vm->getRnd().getRandomNumberRng(0, 1);
+			womanSmellPosition = g_vm->getRnd().getRandomNumberRng(0, 1);
+			break;
+		case 6:
+			showWomanGraffiti = true;
+			showWomanDrawing = true;
+			if (quest != kCreteQuest && quest != kTroyQuest)
+				toughGuyPosition = 1;
+			else 
+				toughGuyPosition = g_vm->getRnd().getRandomNumberRng(0, 1);
+			break;
+		case 7:
+			womanSmellPosition = g_vm->getRnd().getRandomNumberRng(0, 2);
+			if (womanSmellPosition == 2 && (quest != kCreteQuest && quest != kTroyQuest))
+				womanSmellPosition = -1;
+			if (quest == kMedusaQuest)
+				showStatueMan = true;
+			showMotherAndKidSmall = true;
+			showWomanGraffiti = true;
+			break;
+		case 8:
+			if (quest != kCreteQuest && quest != kTroyQuest) {
+				toughGuyPosition = 1;
+				showDrawinWithoutWoman = true;
+				break;
+			}
+
+			if (!persistent->_creteShowMerchant && randomBool()) {
+				toughGuyPosition = 2;
+				showBlondBoy = true;
+			}
+			else
+				toughGuyPosition = 1;
+			showOldMan = true;
+			showDrawinWithoutWoman = true;
+			break;
+		}
+
+		int birdsState = g_vm->getRnd().getRandomNumberRng(0, 2);
+		int dolphinPosition = -1;
+		if (!persistent->_creteShowAtlantisBoat)
+			dolphinPosition = g_vm->getRnd().getRandomNumberRng(0, showOiBoat ? 1 : 2);
+		if (quest != kMedusaQuest && quest != kRescuePhilQuest)
+			_alchemistAmbient = ambient("r2220ba0", "r2220ec0", 5500, 15, 30, AmbientAnim::PAN_RIGHT, kOffsetRightRoom, true);
+
+		// Disable conflicting images
+		if (minotaurPosition == 1
+		    && (toughGuyPosition == 1 || toughGuyPosition == 2))
+			toughGuyPosition = -1;
+		if (minotaurPosition == 1) {
+			showWomanGraffiti = false;
+			showManSelling = false;
+			showOneManBand = false;
+		}
+		if (persistent->_creteShowMerchant &&
+		    (toughGuyPosition == 0 || toughGuyPosition == 1))
+			toughGuyPosition = -1;
+		if (womanSmellPosition == 2)
+			showMotherAndKid = false;
+		if (persistent->_creteShowMerchant)
+			showCat = false;
+		if (quest != kCreteQuest && quest != kTroyQuest)
+			showOldMan = false;
+
+		debug("toughGuyPosition = %d, minotaurPosition = %d, dolphinPosition = %d",
+		      toughGuyPosition, minotaurPosition, dolphinPosition);
+		if (showWomanGraffiti)
+			ambient("r2370ba0", "r2370ea0", 1700, 15, 30, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+		switch (toughGuyPosition) {
+		case 0:
+			ambient("r2340ba0", "r2340ea0", 1400, 15, 25, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+			break;
+		case 1:
+			ambient("r2340bb0", "r2340eb0", 1400, 15, 25, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+			break;
+		case 2:
+			ambient("r2340be0", "r2340ee0", 1600, 15, 25, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+			break;
+		}
+		if (showOneManBand) {
+			room->playAnimLoop(kOneManBandAnim, kOneManBandZ, kOffsetRightRoom);
+			room->enableHotzone(kOneManBandHotZone);
+		}
+		if (showGirlAndNut)
+			ambient("r2140ba0", "r2140ea0", 1500, 15, 30, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+		if (showWomanDrawing)
+			ambient("r2110ba0", "r2110ea0", 1600, 10, 30, AmbientAnim::PAN_LEFT);
+		if (showDrawinWithoutWoman)
+			room->addStaticLayer("r2110bb0", 1600);
+		if (showManSelling)
+			ambient("r2150ba0", "r2150ea0", 1600, 10, 30, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+		if (showMotherAndKid)
+			ambient("r2160ba0", "r2160ea0", 1500, 15, 30, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+		if (showMotherAndKidSmall)
+			ambient("r2160bb0", "", 1500, 15, 30, AmbientAnim::PAN_LEFT);
+		if (showBlondBoy)
+			ambient("r2350ba0", "r2350ea0", 1500, 15, 30, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+		if (showOldMan)
+			ambient("r2100bb0", "r2100eb0", 1500, 10, 30, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+		if (showStatueMan)
+			ambient("r2050ba0", "r2050ea0", 1500, 10, 30, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+		if (showCat) {
+			room->addStaticLayer("r2010on0", kMerchantStandZ);
+			ambient("r2080bb0", "r2080eb0", kMerchantZ, 10, 30, AmbientAnim::PAN_LEFT);
+		}
+
+		if (persistent->_creteShowMerchant) {
+			room->addStaticLayer("r2010on0", kMerchantStandZ);
+			room->selectFrame(kMerchantAnim, kMerchantZ, 0);
+			room->playSound("G0261mA0");
+			if (persistent->_creteShowHorned) {
+				room->addStaticLayer(kHorned, 1220);
+				room->enableHotzone(kHornedHotzone);
+			}
+			if (persistent->_creteShowHornless2) {
+				room->addStaticLayer(kHornless2, 1190);
+				room->setHotzoneEnabled(kHornless2Hotzone,
+					persistent->_creteTriedHornless[0]);
+			}
+			if (persistent->_creteShowHornless1) {
+				room->addStaticLayer(kHornless1, 1180);
+				room->enableHotzone(kHornless1Hotzone);
+			}
+			if (persistent->_creteShowHornless3) {
+				room->addStaticLayer(kHornless3, 1180);
+				room->setHotzoneEnabled(kHornless3Hotzone,
+					persistent->_creteTriedHornless[1]);
+			}
+			if (persistent->_creteShowHornless4) {
+				room->addStaticLayer(kHornless4, 1180);
+				room->setHotzoneEnabled(kHornless4Hotzone,
+					persistent->_creteTriedHornless[2]);
+				g_vm->addTimer(kMerchantIdleTimer, 10000, -1);
+			}
+		}
+
+		if (birdsState == 0) {
+			room->addStaticLayer(kTalusImageWithShip, kTalusZ);
+			room->enableHotzone(kTalusHotzone);
+		} else
+			room->addStaticLayer(kTalusImage, kTalusZ);
+
+		// TODO: fix this
+		switch (minotaurPosition) {
+		case 0:
+			ambient("r2060bf0", "r2060ee0", 1800, 3, 4, AmbientAnim::PAN_RIGHT, kOffsetRightRoom); // shouldn't be an ambient
+			break;
+		case 1:
+			ambient("r2060bt0", "r2060ei0", 1650, 3, 4, AmbientAnim::PAN_RIGHT, kOffsetRightRoom); // shouldn't be an ambient
+			break;
+		case 2:
+//			ambient("r1110ea0", "", 15, 30); // wrong
+			break;
+		case 3:
+			ambient("r1110ba0", "r1110eb0", 850, 20, 40, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+			break;
+		}
+
+		switch (womanSmellPosition) {
+		case 0:
+			ambient("r2380ba0", "r2380ea0", 1550, 15, 30, AmbientAnim::PAN_ANY);
+			break;
+		case 1:
+			ambient("r2380bc0", "r2380ec0", 1550, 15, 30, AmbientAnim::PAN_ANY);
+			break;
+		case 2:
+			ambient("r2380bd0", "r2380ed0", 1550, 15, 30, AmbientAnim::PAN_RIGHT, kOffsetRightRoom);
+			break;
+		}
+
+		switch (birdsState) {
+		case 0:
+			ambient("r1060ba0", "r1060ea0", 4500, 10, 40, AmbientAnim::PAN_ANY);
+			break;
+		case 1:
+			ambient("r1060bb0","r1060eb0", 4500, 5, 40, AmbientAnim::PAN_ANY);
+			break;
+		case 2:
+			ambient("r1060bc0", "r1060ec0", 4500, 5, 40, AmbientAnim::PAN_ANY);
+			break;
+		}
+
+		if (dolphinPosition >= 0) {
+			Common::String dolphinAnim = Common::String::format(
+				"r1150b%c0", 'a' + dolphinPosition);
+			Common::String dolphinSound = Common::String::format(
+				"r1150e%c0", 'a' + dolphinPosition);
+
+			ambient(dolphinAnim, dolphinSound, 5500, 5, 20, AmbientAnim::PAN_LEFT);
+		}
+
+		if (bg1_variant >= 0 && bg2_variant >= 0) {
+			Common::String bg2 = Common::String::format(
+				"r1010o%c1", 'e' + bg2_variant);
+			room->addStaticLayer(bg2, 3500);
+
+			Common::String bg1 = Common::String::format(
+				"r1010o%c0", 'e' + bg1_variant);
+			room->addStaticLayer(bg1, 1000);
+		}
+
+		room->playSound("R1010eA0", true);
+
+		if (g_vm->getPreviousRoomId() == kMinosPalaceRoom) {
+			room->panRightInstant();
+			if (persistent->_creteIntroMerchant) {
+				room->disableMouse();
+				room->panLeftAnim(kIntroMerchantPanFinished);
+				_merchantIsBusy = true;
+				persistent->_creteIntroMerchant = false;
+			}
+
+			if (persistent->_creteIntroAtlantisBoat) {
+				persistent->_creteIntroAtlantisBoat = false;
+				room->disableMouse();
+				room->playVideo("r1180ba0", 1000, 12134, Common::Point(640, 216));
+			}
+
+		}
+
+		if (quest == kMedusaQuest && persistent->_medisleShowFates) {
+			switch (persistent->_creteSandalsState) {
+			case Persistent::SANDALS_NOT_SOLVED:
+				for (int i = 0; i < 4; i++)
+					room->enableHotzone(Common::String::format("VaseSegment%d", i + 1));
+				_vasePos[0] = (g_vm->getRnd().getRandomNumberRng(1, 3) + 2) % 4;
+				_vasePos[1] = (g_vm->getRnd().getRandomNumberRng(1, 5) + 3) % 6;
+				_vasePos[2] = (g_vm->getRnd().getRandomNumberRng(1, 5) + 2) % 6;
+				_vasePos[3] = (g_vm->getRnd().getRandomNumberRng(1, 3) + 3) % 4;
+				renderVase();
+				break;
+			case Persistent::SANDALS_SOLVED:
+				showSandals();
+				break;
+			case Persistent::SANDALS_TAKEN:
+				room->selectFrame("r1220ba0", 500, 0);
+				break;
+			}
+		}
+
+		switch (persistent->_quest) {
+		case kCreteQuest:
+			if (!persistent->_roomVisited[kMinosPalaceRoom]
+			    || persistent->_creteShowAtlantisBoat) {
+				room->playVideo("r1260ma0", 0);
+				break;
+			}
+
+			if (persistent->_creteShowMerchant) {
+				room->playVideo("g0261ma0", 0);
+				break;
+			}
+			break;
+		case kMedusaQuest:
+			if (persistent->_medisleShowFates && persistent->_creteSandalsState == Persistent::SANDALS_NOT_SOLVED)
+				room->playVideo("r1220ma0", 0);
+			break;
+		case kRescuePhilQuest:
+			if (persistent->_creteStrongBoxState == Persistent::BOX_CLOSED || persistent->_creteStrongBoxState == Persistent::BOX_OPEN)
+				room->playVideo("r2230ma0", 0);
+			break;
+		// To silence warning
+		case kTroyQuest:
+		case kNoQuest:
+		case kEndGame:
+		case kNumQuests:
+			break;
+		}
+
+		if (!persistent->_creteSaidHelenPermanentResident && persistent->_quest == kTroyQuest) {
+			persistent->_creteSaidHelenPermanentResident = true;
+			room->disableMouse();
+			room->playVideo("r1250ba0", 0, 12134, Common::Point(0, 216));
+		}
+	}
+
+	void frameCallback() override {
+		if (_strongBoxPopup && strongBoxIsBusy())
+			redrawStrongBox();
+		if (_fadingHades) {
+			Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+			int val = (256 * (g_vm->getCurrentTime() - _fadingHadesStartTime)) / 2000;
+			if (val >= 256) {
+				_fadingHades = false;
+				val = 256;
+				handleEvent(12125);
+			}
+			room->setColorScale("r2035pa0", 256 - val);
+		}
+	}
+
+private:
+	void showMiniStrongBox() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		if (persistent->_quest != kRescuePhilQuest)
+			return;
+
+		switch(persistent->_creteStrongBoxState) {
+		case Persistent::BOX_CLOSED:
+			room->enableHotzone("StrongBoxClosed");
+			room->selectFrame("r2010ba0", 5000, 0, kOffsetRightRoom);
+			break;
+		case Persistent::BOX_OPEN:
+			room->enableHotzone("StrongBoxOpen");
+			room->selectFrame("r2010ba0", 5000, 1, kOffsetRightRoom);
+			break;
+		case Persistent::BOX_OPEN_POTION:
+			room->enableHotzone("StrongBoxOpenPotion");
+			room->selectFrame("r2010ba0", 5000, 2, kOffsetRightRoom);
+			break;
+		case Persistent::BOX_OPEN_NO_POTION:
+			room->enableHotzone("StrongBoxOpenNoPotion");
+			room->selectFrame("r2010ba0", 5000, 3, kOffsetRightRoom);
+			break;
+		}
+	}
+
+	AmbientAnim ambient(const Common::String &anim, const Common::String &sound,
+		     int zValue, int minint, int maxint,
+		     AmbientAnim::PanType pan,
+		     Common::Point offset = Common::Point(0,0),
+		     bool loop = true) {
+		AmbientAnim ret = AmbientAnim(anim, sound, zValue, minint * 1000, maxint * 1000,
+			    loop ? AmbientAnim::KEEP_LOOP : AmbientAnim::DISAPPEAR, offset, pan);
+		ret.start();
+		return ret;
+	}
+
+	bool randomBool() const {
+		return g_vm->getRnd().getRandomNumberRng(0, 1);
+	}
+
+	void renderAtlantisDisks() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->selectFrame(kAtlantisLargeDisk, kAtlantisLargeDiskZ, _atlantisBoatPosition[0]);
+		room->selectFrame(kAtlantisMediumDisk, kAtlantisMediumDiskZ, _atlantisBoatPosition[1]);
+		room->selectFrame(kAtlantisSmallDisk, kAtlantisSmallDiskZ, _atlantisBoatPosition[2]);
+	}
+
+	void advanceAtlantisDisk(int diskNum) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		_atlantisBoatPosition[diskNum] = (_atlantisBoatPosition[diskNum] + 1) % 8;
+		renderAtlantisDisks();
+		room->playSound(Common::String::format("r1210e%c0", 'e' + diskNum), 12303 + diskNum);
+	}
+
+	void hideAtlantisPopupOverlays() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->stopAnim(kAtlantisLargeDisk);
+		room->stopAnim(kAtlantisMediumDisk);
+		room->stopAnim(kAtlantisSmallDisk);
+		room->stopAnim(kAtlantisDiskBackground);
+	}
+
+	void renderVase() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		for(int i = 0; i < 4; i++)
+			room->selectFrame(vaseSegment[i], 1000, _vasePos[i]);
+			
+	}
+
+	void showSandals() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->playAnim("r1220ba0", 500, PlayAnimParams::loop().partial(9, 17));
+		room->enableHotzone("sandals");
+	}
+
+	void showStrongBox() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		// User already clicked, no need to play that anim
+		persistent->_cretePlayedZeusCheckOutThatBox = true;
+		room->pushHotZones("Box.Hot");
+		room->playSound("g0082ea0");
+		redrawStrongBox();
+		_strongBoxPopup = true;
+		switch(persistent->_creteStrongBoxState) {
+		case Persistent::BOX_CLOSED:
+			room->selectFrame("r2230bb0", 1000, 1, kOffsetRightRoom);
+			break;
+		case Persistent::BOX_OPEN:
+			room->selectFrame("r2230bb0", 1000, 3, kOffsetRightRoom);
+			break;
+		case Persistent::BOX_OPEN_POTION:
+			room->selectFrame("r2230bb0", 1000, 3, kOffsetRightRoom);
+			room->selectFrame("r2230bf0", 300, -2, kOffsetRightRoom);
+			break;
+		case Persistent::BOX_OPEN_NO_POTION:
+			room->selectFrame("r2230bb0", 1000, 3, kOffsetRightRoom);
+			room->selectFrame("r2230bf0", 300, -1, kOffsetRightRoom);
+			break;
+		}
+	}
+
+	void hideStrongBox() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->stopAnim("r2230bb0");
+		room->stopAnim("r2230bf0");
+		for (int i = 0; i < 12; i++)
+			room->stopAnim(LayerId("r2010om0", i, "pos"));
+		room->stopAnim(LayerId("r2010om1", 0, "center"));
+		for (int i = 0; i < 4; i++) {
+			room->stopAnim(LayerId("r2010om1", i, "side"));
+		}
+		_strongBoxPopup = false;
+	}
+
+	void redrawStrongBox() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		
+		room->setHotzoneEnabled("Potion", persistent->_creteStrongBoxState == Persistent::BOX_OPEN_POTION);
+		room->setHotzoneEnabled("Latch", persistent->_creteStrongBoxState == Persistent::BOX_CLOSED);
+		room->setHotzoneEnabled("ButtonN", persistent->_creteStrongBoxState == Persistent::BOX_OPEN);
+		room->setHotzoneEnabled("ButtonS", persistent->_creteStrongBoxState == Persistent::BOX_OPEN);
+		room->setHotzoneEnabled("ButtonE", persistent->_creteStrongBoxState == Persistent::BOX_OPEN);
+		room->setHotzoneEnabled("ButtonW", persistent->_creteStrongBoxState == Persistent::BOX_OPEN);
+		room->setHotzoneEnabled("ButtonC", persistent->_creteStrongBoxState == Persistent::BOX_OPEN);
+
+		switch(persistent->_creteStrongBoxState) {
+		case Persistent::BOX_CLOSED:
+			break;
+		case Persistent::BOX_OPEN:
+			room->selectFrame(LayerId("r2010om1", 0, "center"), 400, 0, Common::Point(1060, 308));
+			for (int i = 0; i < 4; i++) {
+				room->selectFrame(LayerId("r2010om1", i, "side"), 400, 1,
+						  strongBoxSideDotOffsets[i]);
+			}
+			{
+				bool wasMoving = strongBoxIsBusy();
+				for (int i = 0; i < 12; i++) {
+					_strongBoxTiles[i].show();
+				}
+				if (wasMoving && !strongBoxIsBusy()) {
+					strongBoxCheckSolution();
+				}
+			}
+			break;
+		case Persistent::BOX_OPEN_POTION:
+		case Persistent::BOX_OPEN_NO_POTION:
+			room->selectFrame(LayerId("r2010om1", 0, "center"), 400, 0, Common::Point(1060, 308));
+			for (int i = 0; i < 4; i++) {
+				room->selectFrame(LayerId("r2010om1", i, "side"), 400, 1,
+						  strongBoxSideDotOffsets[i]);
+			}
+			for (int i = 0; i < 12; i++) {
+				_strongBoxTiles[i].show();
+			}
+			break;
+		}
+	}
+
+	bool strongBoxIsBusy() {
+		for (int i = 0; i < 12; i++) {
+			if (_strongBoxTiles[i].isMoving()) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	void strongBoxCheckSolution() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		bool zOk = false, eOk = false, uOk = false, sOk = false;
+		for (int i = 0; i < 12; i++) {
+			if (_strongBoxTiles[i].getPosition() == 0
+			    && _strongBoxTiles[i].getLetter() == StrongBoxTile::kLetterZ
+			    && (_strongBoxTiles[i].getOrientation() == 0
+				|| _strongBoxTiles[i].getOrientation() == 180))
+				zOk = true;
+			if (_strongBoxTiles[i].getPosition() == 1
+			    && _strongBoxTiles[i].getLetter() == StrongBoxTile::kLetterE
+			    && _strongBoxTiles[i].getOrientation() == 0)
+				eOk = true;
+			if (_strongBoxTiles[i].getPosition() == 2
+			    && _strongBoxTiles[i].getLetter() == StrongBoxTile::kLetterU
+			    && _strongBoxTiles[i].getOrientation() == 0)
+				uOk = true;
+			if (_strongBoxTiles[i].getPosition() == 3
+			    && _strongBoxTiles[i].getLetter() == StrongBoxTile::kLetterS
+			    && (_strongBoxTiles[i].getOrientation() == 0
+				|| _strongBoxTiles[i].getOrientation() == 180))
+				sOk = true;
+		}
+
+		if (zOk && eOk && uOk && sOk) {
+			persistent->_creteStrongBoxState = Persistent::BOX_OPEN_POTION;
+			room->disableMouse();
+			room->enableHotzone("Potion");
+			room->disableHotzone("ButtonS");
+			room->disableHotzone("ButtonN");
+			room->disableHotzone("ButtonE");
+			room->disableHotzone("ButtonW");
+			room->disableHotzone("ButtonC");
+			room->playAnimWithSound("r2230bf0", "r2230ea0", 300, PlayAnimParams::keepLastFrame().partial(0, -2),
+						12414, kOffsetRightRoom);
+		}
+	}
+
+	void strongBoxMoveTiles(int p1, int p2, int p3, int p4) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->playSound("r2230ee0");
+		for (int i = 0; i < 12; i++) {
+			int pos = _strongBoxTiles[i].getPosition();
+			if (pos == p1)
+				_strongBoxTiles[i].moveTo(p2);
+			if (pos == p2)
+				_strongBoxTiles[i].moveTo(p3);
+			if (pos == p3)
+				_strongBoxTiles[i].moveTo(p4);
+			if (pos == p4)
+				_strongBoxTiles[i].moveTo(p1);
+		}
+	}
+
+	bool _fadingHades;
+	int _fadingHadesStartTime;
+	int _tavernCounter;
+	int _oneManBandCounter;
+	int _atlantisBoatPosition[3];
+	bool _merchantIsBusy;
+	int _vasePos[4];
+	bool _vaseBusy[4];
+	bool _strongBoxPopup;
+	StrongBoxTile _strongBoxTiles[12];
+	Common::Array<Common::String> _tavernTalks;
+	AmbientAnim _alchemistAmbient;
+};
+
+Common::SharedPtr<Hadesch::Handler> makeCreteHandler() {
+	return Common::SharedPtr<Hadesch::Handler>(new CreteHandler());
+}
+
+}
diff --git a/engines/hadesch/rooms/daedalus.cpp b/engines/hadesch/rooms/daedalus.cpp
new file mode 100644
index 0000000000..9f29a8660c
--- /dev/null
+++ b/engines/hadesch/rooms/daedalus.cpp
@@ -0,0 +1,361 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+#include "hadesch/ambient.h"
+
+namespace Hadesch {
+
+static const char *kDaedalusStillFrame = "daedalus still frame";
+static const char *kDaedalusAmbient = "daedalus ambient";
+static const char *kModelPiece = "model piece";
+static const char *kLabyrinthWorkers = "labyrinth workers";
+
+enum {
+	kDaedalusTick = 13901,
+
+	// TODO: Remove this once we have a possibility
+	// to pass an argument to event handler.
+	// Originally: "daedalus intro 1" -> 13002[1] -> "phil intro 1"
+	// -> 13003[1] -> "daedalus intro 2" -> 13002[2]
+	// -> "phil intro 2" -> 13003[2] -> "daedalus intro 3"
+	kIntroStep1 = 1013001,
+	kIntroStep2 = 1013002,
+	kIntroStep3 = 1013003,
+	kIntroStep4 = 1013004,
+	kIntroStep5 = 1013005
+};
+
+enum {
+	kBackgroundZ = 10000,
+	kLabyrinthWorkersZ = 900,
+	kDaedalusZ = 500,
+	kModelPieceZ = 500,
+	kPhilZ = 0
+};
+
+class DaedalusHandler : public Handler {
+public:
+	DaedalusHandler() {
+		_daedalusIsBusy = false;
+	}
+
+	void handleClick(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+
+		if (name == "minos palace") {
+			g_vm->moveToRoom(kMinosPalaceRoom);
+			return;
+		}
+
+		if (name == "daedalus") {
+			playDaedalusVideo("daedalus no materials", 13005, Common::Point(76, 0));
+			return;
+		}
+
+		if (name == "wings") {
+			playDaedalusVideo("daedalus wings", 4009, Common::Point(10, 56));
+			return;
+		}
+
+		if (name == "labyrinth" && persistent->_quest != kCreteQuest) {
+			room->disableMouse();
+			room->playVideo("phil navigation help", 0, 13007, Common::Point(0, 216));
+			return;
+		}
+
+		if (name == "brick wall") {
+			daedalusWallMotion();
+			return;
+		}
+	}
+
+	bool handleClickWithItem(const Common::String &name, InventoryItem item) override {
+		Persistent *persistent = g_vm->getPersistent();
+
+		int labItem = -1;
+		debug("Item is %d", item);
+		switch (item) {
+		case kStone:
+			labItem = 0;
+			break;
+		case kBricks:
+			labItem = 1;
+			break;
+		case kWood:
+			labItem = 2;
+			break;
+		case kStraw:
+			labItem = 3;
+			break;
+		default:
+			labItem = -1;
+			break;
+		}
+		
+		if ((name == "daedalus" || name == "chute") && labItem < 0) {
+			playDaedalusVideo("daedalus what to do with that", 13005, Common::Point(10, 40));
+			return true;
+		}
+
+		if (name == "daedalus" && labItem >= 0) {
+			playDaedalusVideo("daedalus put that in the chute", 4009, Common::Point(64, 48));
+			return true;
+		}
+
+		if (name == "chute" && labItem >= 0) {
+			bool hasAll = true;
+			Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+			g_vm->getHeroBelt()->removeFromInventory(item);
+			persistent->_daedalusLabItem[labItem] = true;
+			for (int i = 0; i < 4; i++) {
+				if (!persistent->_daedalusLabItem[i]) {
+					hasAll = false;
+					break;
+				}
+			}
+
+			renderCheckMarks();
+
+			room->playAnimWithSound("dust cloud", "dust cloud sound", 850, PlayAnimParams::disappear());
+
+			if (hasAll) 
+				playDaedalusVideo("daedalus exclaims", 13008, Common::Point(0, 2));
+			else {
+				// Original goes to event 4009
+				if (g_vm->getRnd().getRandomNumberRng(0, 1))
+					playDaedalusVideo("daedalus congrats 1", 4009, Common::Point(70, 30));
+				else
+					playDaedalusVideo("daedalus congrats 2", 4009, Common::Point(68, 32));
+			}
+
+			return true;
+		}
+
+		return false;
+	}
+
+	void handleEvent(int eventId) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		switch(eventId) {
+		case kIntroStep1:
+			room->playVideo("phil intro 1", kPhilZ, kIntroStep2,
+					Common::Point(0, 216));
+			room->selectFrame(kDaedalusAmbient, kDaedalusZ, 0);
+			break;
+		case kIntroStep2:
+			playDaedalusVideo("daedalus intro 2", kIntroStep3,
+					  Common::Point(76, 55));
+			break;
+		case kIntroStep3:
+			room->playVideo("phil intro 2", kPhilZ, kIntroStep4,
+					Common::Point(0, 216));
+			room->selectFrame(kDaedalusStillFrame, kDaedalusZ, 0);
+			break;
+
+		case kIntroStep4:
+			playDaedalusVideo("daedalus intro 3", kIntroStep5,
+					  Common::Point(76, 60));
+			break;
+		case 13004:
+			room->stopAnim("daedalus note");
+			room->stopAnim("daedalus note text male");
+			room->stopAnim("daedalus note text female");
+			break;
+		case 13005:
+			g_vm->addTimer(13006, 5000, 1);
+			// Fallthrough
+		case kIntroStep5:
+		case 4009:
+			daedalusBecomesIdle();
+			room->enableMouse();
+			break;
+		case 13006:
+			if (!room->isMouseEnabled()) {
+				g_vm->addTimer(13006, 5000, 1);
+				break;
+			}
+			room->disableMouse();
+			room->playVideo("phil coerces", 0, 13007, Common::Point(0, 216));
+			break;
+		case 13007:
+			room->enableMouse();
+			break;
+		case 13008:
+			room->enableMouse();
+			room->selectFrame("daedalus exclaims still", kDaedalusZ,0);
+			// TODO: for now we skip arcade sequence until it's implemented
+			//			g_vm->moveToRoom(kMinotaurPuzzle);
+			g_vm->moveToRoom(kQuiz);
+			break;
+		case 13011: {
+			// TODO: use right algorithm
+			int roarNum = g_vm->getRnd().getRandomNumberRng(1, 5);
+			room->playSound(Common::String::format("ambient minotaur roar %d", roarNum), 13012);
+			break;
+		}
+		case 13012:
+			g_vm->addTimer(13011, g_vm->getRnd().getRandomNumberRng(5000, 10000));
+			break;
+		case kDaedalusTick:
+			if (_daedalusIsBusy)
+				break;
+			_daedalusIsBusy = true;
+			switch (g_vm->getRnd().getRandomNumberRng(1, 6)) {
+			case 1:
+				daedalusWallMotion();
+				break;
+			case 2:
+			case 3:
+			case 4:
+				room->playAnim(kDaedalusAmbient, kDaedalusZ, PlayAnimParams::keepLastFrame().partial(0, 21), 13904);
+				break;
+			case 5:
+			case 6:
+				room->playAnim(kDaedalusAmbient, kDaedalusZ, PlayAnimParams::keepLastFrame().partial(23, 30), 13903);
+				break;
+			}
+			break;
+		case 13902:
+			room->playAnim(kDaedalusAmbient, kDaedalusZ, PlayAnimParams::keepLastFrame().partial(35, -1), 13904);
+			room->playAnimWithSound(kLabyrinthWorkers, "labyrinth workers sound", kLabyrinthWorkersZ,
+						PlayAnimParams::keepLastFrame());
+			break;
+		case 13903:
+			room->playAnim(kDaedalusAmbient, kDaedalusZ, PlayAnimParams::keepLastFrame().partial(57, -1), 13904);
+			break;
+		case 13904:
+			_daedalusIsBusy = false;
+			break;
+		}
+	}
+
+	void prepareRoom() override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		Quest quest = persistent->_quest;
+		room->loadHotZones("Daedalus.HOT", false);
+		room->addStaticLayer("background", kBackgroundZ);
+		room->addStaticLayer("chute label", 850);
+		g_vm->getHeroBelt()->setColour(HeroBelt::kWarm);
+
+		if (quest == kCreteQuest) {
+			room->addStaticLayer("wings", 900);
+			room->addStaticLayer("check list", 800);
+			room->addStaticLayer("check list text", 799);
+			room->enableHotzone("brick wall");
+			room->enableHotzone("chute");
+			room->enableHotzone("wings");
+			room->enableHotzone("daedalus");
+			room->selectFrame(kLabyrinthWorkers, kLabyrinthWorkersZ, 0);
+			room->selectFrame(kDaedalusAmbient, kDaedalusZ, 0);
+			g_vm->addTimer(13011, g_vm->getRnd().getRandomNumberRng(5000, 10000));
+			g_vm->addTimer(kDaedalusTick, g_vm->getRnd().getRandomNumberRng(5000, 10000), -1);
+		} else {
+			room->enableHotzone("labyrinth");
+			if (!persistent->_daedalusShowedNote) {
+				persistent->_daedalusShowedNote = true;
+				room->selectFrame("daedalus note", 800, 0);
+				room->selectFrame(persistent->_gender == kMale ? "daedalus note text male"
+						  : "daedalus note text female", 799, 0);
+				room->playSound(persistent->_gender == kMale ? "daedalus note vo male"
+						: "daedalus note vo female", 13004);
+			}
+		}
+
+		renderCheckMarks();
+
+		room->enableHotzone("minos palace");
+
+		if (quest == kCreteQuest
+		    && !persistent->isRoomVisited(kDaedalusRoom)) {
+			persistent->_creteIntroAtlantisBoat = true;
+			persistent->_creteShowAtlantisBoat = true;
+			persistent->_creteIntroAtlantisWood = true;
+			persistent->_troyPlayAttack = true;
+			playDaedalusVideo("daedalus intro 1", kIntroStep1,
+					  Common::Point(50, 35));
+			room->playSoundLoop("theme music 1");
+		} else if (quest == kCreteQuest &&
+			   (persistent->_daedalusLabItem[0] || persistent->isInInventory(kStone)) &&
+			   (persistent->_daedalusLabItem[1] || persistent->isInInventory(kBricks)) &&
+			   (persistent->_daedalusLabItem[2] || persistent->isInInventory(kWood)) &&
+			   (persistent->_daedalusLabItem[3] || persistent->isInInventory(kStraw))) {
+			room->playSoundLoop("theme music 2");
+		} else {
+			room->playSoundLoop("R4010eA0");
+		}
+		AmbientAnim("mouse", "mouse sound", 900, 5000, 10000, AmbientAnim::KEEP_LOOP, Common::Point(0, 0),
+			    AmbientAnim::PAN_ANY).start();
+	}
+private:
+	void playDaedalusVideo(const Common::String &name, int callback, const Common::Point &offset) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+
+		_daedalusIsBusy = true;
+
+		room->stopAnim(kDaedalusStillFrame);
+		room->stopAnim(kDaedalusAmbient);
+		room->selectFrame(kModelPiece, kModelPieceZ, 0);
+		room->disableMouse();
+		room->playVideo(name, kDaedalusZ, callback, offset);
+	}
+
+	void daedalusBecomesIdle() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+
+		room->selectFrame(kDaedalusAmbient, kDaedalusZ, 0);
+		room->stopAnim(kModelPiece);
+		_daedalusIsBusy = false;
+	}
+
+	void renderCheckMarks() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		for (int i = 0; i < 4; i++) {
+			Common::String layer = Common::String::format("check mark %d", i + 1);
+			if (persistent->_daedalusLabItem[i]) {
+				room->selectFrame(layer, 798, 0);
+			} else {
+				room->stopAnim(layer);
+			}
+		}
+	}
+
+	void daedalusWallMotion() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->playAnim(kDaedalusAmbient, kDaedalusZ, PlayAnimParams::keepLastFrame().partial(0, 34), 13902);
+		room->playSound("daedalus ambient sound");
+		_daedalusIsBusy = true;
+	}
+
+	bool _daedalusIsBusy;
+};
+
+Common::SharedPtr<Hadesch::Handler> makeDaedalusHandler() {
+	return Common::SharedPtr<Hadesch::Handler>(new DaedalusHandler());
+}
+
+}
diff --git a/engines/hadesch/rooms/ferry.cpp b/engines/hadesch/rooms/ferry.cpp
new file mode 100644
index 0000000000..fcc42f5f1e
--- /dev/null
+++ b/engines/hadesch/rooms/ferry.cpp
@@ -0,0 +1,1074 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+
+namespace Hadesch {
+
+enum {
+	kBackgroundZ = 10000,
+	kCharonZ = 701
+};
+
+static const int kNumSeats = 10;
+
+enum {
+	kHuman          = 1 <<  0,
+	kMonster        = 1 <<  1,
+	kAnimal         = 1 <<  2,
+	kSmoking        = 1 <<  6,
+	kFlat           = 1 <<  7,
+	kFrozen         = 1 <<  8,
+	kPierced        = 1 <<  9,
+	kWet            = 1 << 10,
+	kDismembered    = 1 << 11,
+	kWeaponInjury   = 1 << 12,
+	kTridentInjured = 1 << 13,
+	kHeadInjury     = 1 << 14,
+	kDrownedGuy     = 1 << 15,
+	kHorned         = 1 << 16,
+	kCrushed        = 1 << 17,
+	kSnakeKilled    = 1 << 18,
+	kCold           = 1 << 19,
+	kTwoHeaded      = 1 << 20,
+	kHot            = 1 << 21,
+	kHeadless       = 1 << 22,
+	kChokedDog      = 1 << 23,
+	kWithHoles      = 1 << 24,
+	kCat            = 1 << 25,
+	kDog            = 1 << 26
+};
+
+static const struct {
+	const char *image;
+	const char *nameImage;
+	const char *name;
+	int waityoffset;
+	int priorityleft;
+	int priorityright;
+	int attributes;
+	Common::Point waitBubble;
+	Common::Point starboardBubble;
+	Common::Point portBubble;
+	int portFrame;
+	int starboardFrame;
+	const char *animSound;
+	int animPlayCount;
+} shadows[] = {
+	{
+	 "xxxxxxxx", ""        , "Chariot Wheel Guy" ,   0,  0, 0   , kHuman   | kFlat | kCrushed,
+	 Common::Point(0, 0), Common::Point(0, 0), Common::Point(0, 0), 2, 3,
+	 nullptr, 0
+	},
+	{"V9070bD0", "V9520tD0", "Chariot Wheel Dog" ,  40, 20, 5   , kAnimal  | kFlat | kCrushed | kDog,
+	 Common::Point(113, 177), Common::Point(66, 134), Common::Point(106, 176), 16, 32,
+	 nullptr, 0
+	},
+	{"V9070bE0", "V9520tE0", "Drowned Guy"       ,   0, 10, 15  , kHuman   | kWet | kDrownedGuy,
+	 Common::Point(124, 84), Common::Point(78, 67), Common::Point(104, 80), 19, 35,
+	 "V9070eE0", 1
+	},
+	{"V9070bI0", "V9520tI0", "Holey Guy"         ,   0, 15, 20  , kHuman   | kPierced | kWeaponInjury | kWithHoles,
+	 Common::Point(84, 107), Common::Point(68, 66), Common::Point(92, 100), 6, 13,
+	 nullptr, 0
+	},
+	{"V9070bP0", "V9520tO0", "Cyclops"           ,  14, 50, 50  , kMonster | kPierced | kWeaponInjury | kHeadInjury | kCrushed | kTwoHeaded,
+	 Common::Point(77, 64), Common::Point(73, 72), Common::Point(63, 68), 14, 33,
+	 nullptr, 0
+	},
+	{"V9070bM0", "V9520tM0", "Toasted Guy"       , -13, 40, 50  , kHuman   | kSmoking | kHot,
+	 Common::Point(115, 89), Common::Point(78, 71), Common::Point(104, 73), 14, 28,
+	 nullptr, 0
+	},
+	{"V9070bJ0", "V9520tJ0", "Minotaur"          ,   0, 25, 20  , kMonster | kHorned,
+	 Common::Point(111, 100), Common::Point(63, 79), Common::Point(106, 111), 12, 45,
+	 nullptr, 0
+	},
+	{"V9070bA0", "V9520tA0", "Ball Chain Monster",   0, 20, 20  , kMonster | kWeaponInjury | kHeadInjury | kHeadless,
+	 Common::Point(79, 92), Common::Point(85, 91), Common::Point(83, 102), 13, 26,
+	 nullptr, 0
+	},
+	{"V9070bL0", "V9520tL0", "Snake Man"         ,   0, 25, 40  , kHuman   | kAnimal | kCrushed | kSnakeKilled,
+	 Common::Point(82, 65), Common::Point(93, 62), Common::Point(74, 45), 4, 8,
+	 "V9070eL0", 3
+	},
+	{"xxxxxxxx", ""	       , "Arrow Guy"         ,   0,  0, 0   , kHuman   | kPierced | kWeaponInjury,
+	 Common::Point(0, 0), Common::Point(0, 0), Common::Point(0, 0), 0, 0,
+	 nullptr, 0 },
+	// 10
+	{"V9070bC0", "V9520tC0", "Choked Dog"        ,  46, 30, 5   , kAnimal  | kDog | kChokedDog,
+	 Common::Point(97, 91), Common::Point(83, 85), Common::Point(81, 85), 11, 22,
+	 "V9070eC0", 2
+	},
+	{"V9070bK0", "V9520tK0", "Mounted Aries"     ,  23, 35, 40  , kAnimal  | kHeadInjury | kHorned,
+	 Common::Point(121, 62), Common::Point(75, 54), Common::Point(113, 52), 6, 12,
+	 "V9070eK0", 1
+	},
+	{"V9070bF0", "V9520tF0", "Flat Cat"          ,  43, 40, 5   , kAnimal  | kFlat | kCrushed | kCat,
+	 Common::Point(75, 142), Common::Point(72, 142), Common::Point(72, 142), 5, 19,
+	 nullptr, 0
+	},
+	{"V9070bH0", "V9520tH0", "Headless Guy"      , -14,  5, 15  , kHuman   | kDismembered | kWeaponInjury | kHeadInjury | kHeadless,
+	 Common::Point(110, 119), Common::Point(50, 98), Common::Point(115, 117), 26, 52,
+	 nullptr, 0
+	},
+	{"V9070bG0", "V9520tG0", "Frozen Guy"        ,   0, 15, 40  , kHuman   | kCold | kWet | kFrozen,
+	 Common::Point(71, 67), Common::Point(91, 80), Common::Point(74, 58), 9, 18,
+	 "V9070eG0", 2
+	},
+	// 15
+	{"xxxxxxxx", ""	       , "Caesar"            ,   0,  0, 0   , kHuman   | kPierced | kWeaponInjury,
+	 Common::Point(0, 0), Common::Point(0, 0), Common::Point(0, 0), 0, 0,
+	 nullptr, 0 },
+	{"V9070bN0", "V9520tN0", "Trident Guy"       ,   9,  5, 10  , kHuman   | kPierced | kWeaponInjury | kHeadInjury | kTridentInjured,
+	 Common::Point(89, 91), Common::Point(81, 88), Common::Point(94, 88), 5, 10,
+	 nullptr, 0
+	},
+	{"xxxxxxxx", ""	       , "BeeSting Guy"      ,   0,  0, 0   , kHuman   | kPierced,
+	 Common::Point(0, 0), Common::Point(0, 0), Common::Point(0, 0), 0, 0,
+	 nullptr, 0 },
+	{"xxxxxxxx", ""	       , "Half Man"          ,   0,  0, 0   , kHuman   | kDismembered,
+	 Common::Point(0, 0), Common::Point(0, 0), Common::Point(0, 0), 0, 0,
+	 nullptr, 0 },
+	{"V9070bQ0", "V9520tP0", "Toasted Cat"       ,  41, 45, 5   , kAnimal  | kSmoking | kHot | kCat,
+	 Common::Point(111, 158), Common::Point(59, 135), Common::Point(105, 161), 6, 12,
+	 "V9070eQ0", 3
+	},
+	// 20
+	{"V9070bR0", "V9520tQ0", "Pillar Guy"        ,   0, 15, 10  , kHuman   | kFlat | kHeadInjury | kCrushed,
+	 Common::Point(86, 188), Common::Point(78, 186), Common::Point(92, 186), 15, 30,
+	 "V9070eR0", 1
+	},
+	{"xxxxxxxx", ""	       , "Ax-Head Guy"       ,   0,  0, 0   , kHuman   | kWeaponInjury | kHeadInjury,
+	 Common::Point(0, 0), Common::Point(0, 0), Common::Point(0, 0), 0, 0,
+	 nullptr, 0 },
+	{"V9070bB0", "V9520tB0", "Boiled Guy"        ,  45,  5, 10  , kHuman   | kHot | kWet,
+	 Common::Point(80, 62), Common::Point(81, 68), Common::Point(79, 60), 13, 42,
+	 nullptr, 0
+	}
+};
+
+static const struct {
+	const char *image;
+	const char *sound;
+} thoughts[] = {
+	{ "V9140tA0", "V9140nA0" }, // 0
+	{ "V9140tB0", "V9140nB0" },
+	{ "V9140tC0", "V9140nC0" },
+	{ "V9140tD0", "V9140nD0" },
+	{ "V9140tE0", "V9140nE0" },
+	{ "V9140tF0", "V9140nF0" }, // 5
+	{ "V9140tG0", "V9140nG0" },
+	{ "V9140tH0", "V9140nH0" },
+	{ "V9140tI0", "V9140nI0" },
+	{ "V9140tJ0", "V9140nJ0" },
+	{ "V9140tK0", "V9140nK0" }, // 10
+	{ "V9140tL0", "V9140nL0" },
+	{ "V9140tM0", "V9140nM0" },
+	{ "V9140tN0", "V9140nN0" },
+	{ "V9140tO0", "V9140nO0" },
+	{ "V9140tP0", "V9140nP0" }, // 15
+	{ "V9140tQ0", "V9140nQ0" },
+	{ "V9140tR0", "V9140nR0" },
+	{ "V9140tS0", "V9140nS0" },
+	{ "V9140tT0", "V9140nT0" },
+	{ "V9140tU0", "V9140nU0" }, // 20
+	{ "V9140tV0", "V9140nV0" },
+	{ "V9140tW0", "V9140nW0" },
+	{ "V9160tA0", "V9160nA0" },
+	{ "V9160tB0", "V9160nB0" },
+	{ "V9160tC0", "V9160nC0" }, // 25
+	{ "V9160tD0", "V9160nD0" },
+	{ "V9160tE0", "V9160nE0" },
+	{ "V9160tF0", "V9160nF0" },
+	{ "V9160tG0", "V9160nG0" },
+	{ "V9160tH0", "V9160nH0" }, // 30
+	{ "V9160tI0", "V9160nI0" },
+	{ "V9160tJ0", "V9160nJ0" },
+	{ "V9160tK0", "V9160nK0" },
+	{ "V9160tL0", "V9160nL0" },
+	{ "V9170tA0", "V9170nA0" }, // 35
+	{ "V9170tB0", "V9170nB0" },
+	{ "V9170tC0", "V9170nC0" },
+	{ "V9170tD0", "V9170nD0" },
+	{ "V9170tE0", "V9170nE0" },
+	{ "V9170tF0", "V9170nF0" }, // 40
+	{ "V9170tG0", "V9170nG0" },
+	{ "V9170tH0", "V9170nH0" },
+	{ "V9170tI0", "V9170nI0" },
+	{ "V9170tJ0", "V9170nJ0" },
+	{ "V9170tK0", "V9170nK0" }, // 45
+	{ "V9170tL0", "V9170nL0" }, 
+	{ "V9170tM0", "V9170nM0" },
+	{ "V9170tN0", "V9170nN0" },
+	{ "V9170tO0", "V9170nO0" },
+	{ "V9170tP0", "V9170nP0" }, // 50
+	{ "V9170tQ0", "V9170nQ0" },
+	{ "V9170tR0", "V9170nR0" },
+	{ "V9170tS0", "V9170nS0" },
+	{ "V9170tT0", "V9170nT0" },
+	{ "V9170tU0", "V9170nU0" }, // 55
+	{ "V9170tV0", "V9170nV0" },
+	{ "V9170tY0", "V9170nY0" },
+	{ "V9200tA0", "V9200nA0" },
+	{ "V9200tB0", "V9200nB0" },
+	{ "V9200tC0", "V9200nC0" }, // 60
+	{ "V9200tD0", "V9200nD0" },
+	{ "V9200tE0", "V9200nE0" },
+	{ "V9200tF0", "V9200nF0" },
+	{ "V9200tG0", "V9200nG0" },
+	{ "V9200tH0", "V9200nH0" }, // 65
+	{ "V9200tI0", "V9200nI0" },
+	{ "V9200tJ0", "V9200nJ0" },
+	{ "V9200tK0", "V9200nK0" },
+	{ "V9200tL0", "V9200nL0" },
+	{ "V9200tM0", "V9200nM0" }, // 70
+	{ "V9200tN0", "V9200nN0" },
+	{ "V9200tO0", "V9200nO0" },
+	{ "V9240tA0", "V9240nA0" },
+	{ "V9210tA0", "V9210nA0" },
+	{ "V9210tB0", "V9210nB0" }, // 75
+	{ "V9210tC0", "V9210nC0" },
+	{ "V9210tD0", "V9210nD0" },
+	{ "V9220tA0", "V9220nA0" },
+	{ "V9220tB0", "V9220nB0" },
+	{ "V9220tC0", "V9220nC0" }, // 80
+	{ "V9220tD0", "V9220nD0" },
+        { "V9245tA0", "V9300nE0" }
+};
+
+static const struct {
+	const char *image;
+	const char *sound;
+} charonRules[] = {
+	{ "V9250tA0", "V9250aA0" }, // 0
+	{ "V9250tB0", "V9250aB0" },
+	{ "V9250tC0", "V9250aC0" },
+	{ "V9250tD0", "V9250aD0" },
+	{ "V9260tA0", "V9260nA0" },
+	{ "V9260tB0", "V9260nB0" }, // 5
+	{ "V9260tC0", "V9260nC0" },
+	{ "V9260tD0", "V9260nD0" },
+	{ "V9260tE0", "V9260nE0" },
+	{ "V9260tF0", "V9260nF0" },
+	{ "V9260tG0", "V9260nG0" }, // 10
+	{ "V9260tJ0", "V9260nJ0" },
+	{ "V9270tA0", "XXXXXXXX" }
+};
+
+static const char *charonFinishSounds[] = {
+	"V9300nA0",
+	"V9300nH0",
+	"V9300nI0"
+};		
+
+static const char *charonAnims[] = {
+	"V9140BA0",
+	"V9140BB0",
+	"V9140BC0"
+};
+
+static struct {
+	const char *name;
+	Common::Point offset;
+} charonIdleVideos[] = {
+	{"V9140BD0", Common::Point(418, 40)},
+	{"V9140BE0", Common::Point(370, 64)}
+};
+
+struct Shade {
+	int shadowId;
+	int thoughtId;
+	int currentPos;
+	int waitingPos;
+	bool positionIsFixed;
+	int rowidx;
+	int tabNA;
+	int tabN;
+	int tabO;
+	int tabA;
+	int tabNN;
+};
+
+struct ImagePos {
+	int x, y, z;
+
+	Common::Point getPoint() const {
+		return Common::Point(x, y);
+	}
+};
+
+static const ImagePos boatPosition[] = {
+	{150, 380, 555},
+	{210, 352, 556},
+	{266, 328, 557},
+	{321, 307, 558},
+	{381, 293, 559},
+	{240, 452, 550},
+	{310, 440, 551},
+	{373, 425, 552},
+	{434, 405, 553},
+	{491, 383, 554}
+};
+
+static const ImagePos waitPosition[] = {
+	{41,  119, 1058},
+	{114,  94, 1059},
+	{187,  69, 1060},
+	{41,  226, 1051},
+	{114, 201, 1052},
+	{187, 176, 1053},
+	{260, 151, 1054},
+	{333, 126, 1055},
+	{406, 101, 1056},
+	{479,  76, 1057}
+};
+
+enum {
+	kCharonEndTalk = 24812,
+	// 24802 is split into 2 to avoid having an argument
+	k24802_arg0 = 1024801,
+	k24802_arg1 = 1024802,
+	k24017_arg0 = 1024001,
+	k24017_arg9 = 1024010,
+	k24018_arg0 = 1024011,
+	k24018_arg9 = 1024020,
+	k24801_arg1 = 1024021,
+	k24801_arg2 = 1024022,
+	k24801_arg3 = 1024023,
+	k24801_arg4 = 1024024,
+	k24801_arg5 = 1024025,
+	k24801_arg6 = 1024026
+};
+
+class FerryHandler : public Handler {
+public:
+	FerryHandler() {
+		_dragged = -1;
+		_clickTimer = -1;
+		_isPlayingYuck = false;
+		_lastCharonAnim = -1;
+		_charonIsBusy = false;
+		memset(_isInAnim, 0, sizeof(_isInAnim));
+	}
+
+	void handleClick(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+
+		if (name.matchString("s##")) {
+			g_vm->addTimer(24012, 350);
+			_clickTimer = typeToIdx(name.substr(1).asUint64());
+			return;
+		}
+
+		if (name.matchString("f##")) {
+			int fPos = (name[1] - '0') * 5 + (name[2] - '0');
+			for (unsigned i = 0; i < _shades.size(); i++) {
+				if (_shades[i].currentPos == fPos) {
+					if (_shades[i].positionIsFixed)
+						showThoughtByShadowId(i);
+					else {
+						_clickTimer = i;
+						g_vm->addTimer(24012, 350);
+					}
+					break;
+				}
+			}
+			return;
+		}
+
+		if (name == "Sign" && _charonTID != 12) {
+			playCharonSound(charonRules[_charonTID].sound);
+			return;
+		}
+
+		/*
+		  TODO:
+ MNSH: Charon
+ MNSH: sp##
+ MNSH: ss##
+*/
+	}
+
+	void handleUnclick(const Common::String &name, const Common::Point pnt) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		if (_clickTimer >= 0) {
+			g_vm->cancelTimer(24012);
+			showThoughtByShadowId(_clickTimer);
+			_clickTimer = -1;
+		} else if (_dragged >= 0) {
+			if (name.matchString("f##")) {
+				moveToFerry(_dragged, (name[1] - '0') * 5
+					    + (name[2] - '0'));
+			} else {
+				backToWaiting(_dragged);
+			}
+			room->stopAnim("v9010bc0");
+			_dragged = -1;
+			hideThought();
+			levelRender();
+		}
+	}
+
+	void handleMouseOver(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		if (name.matchString("f##") && _dragged != -1) {
+			room->selectFrame("v9010bc0", 800, name[1] == '1' ? (9 - (name[2] - '0')) : (name[2] - '0'));
+		}
+	}
+
+	void handleMouseOut(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		if (name.matchString("f##") && _dragged != -1) {
+			room->stopAnim("v9010bc0");
+		}
+	}
+
+	void handleEvent(int eventId) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+
+		switch (eventId) {
+		case 24006:
+			if (_count24006++ >= 3) {
+				hideThought();
+				break;
+			}
+			g_vm->addTimer(24006, 1200);
+			// TODO: don't issue duplicates
+			showThoughtByShadowId(g_vm->getRnd().getRandomNumberRng(0, 9), true);
+			break;
+		case 24010:
+			_isPlayingYuck = false;
+			break;
+		case 24012:
+			_dragged = _clickTimer;
+			_clickTimer = -1;
+			levelRender();
+			break;
+		case k24017_arg0:
+		case k24017_arg0 + 1:
+		case k24017_arg0 + 2:
+		case k24017_arg0 + 3:
+		case k24017_arg0 + 4:
+		case k24017_arg0 + 5:
+		case k24017_arg0 + 6:
+		case k24017_arg0 + 7:
+		case k24017_arg0 + 8:
+		case k24017_arg9:
+		{
+			int shade = eventId - k24017_arg0;
+			g_vm->addTimer(k24017_arg0 + shade, g_vm->getRnd().getRandomNumberRng(10200, 21800));
+			if (shade == _dragged)
+				return;
+			if (_shades[shade].currentPos >= 5) {
+				idleAnimShade(shade, boatPosition[_shades[shade].currentPos].z,
+					      shadows[_shades[shade].shadowId].portFrame,
+					      shadows[_shades[shade].shadowId].starboardFrame - 1);
+				return;
+			}
+
+			if (_shades[shade].currentPos >= 0) {
+				idleAnimShade(shade, boatPosition[_shades[shade].currentPos].z,
+					      shadows[_shades[shade].shadowId].starboardFrame, -1);
+				return;
+			}
+
+			if (_shades[shade].waitingPos >= 0) {
+				idleAnimShade(shade, waitPosition[_shades[shade].waitingPos].z,
+					      0, shadows[_shades[shade].shadowId].portFrame - 1);
+				return;
+			}
+
+			break;
+		}
+		case k24018_arg0:
+		case k24018_arg0 + 1:
+		case k24018_arg0 + 2:
+		case k24018_arg0 + 3:
+		case k24018_arg0 + 4:
+		case k24018_arg0 + 5:
+		case k24018_arg0 + 6:
+		case k24018_arg0 + 7:
+		case k24018_arg0 + 8:
+		case k24018_arg9: {
+			uint shade = eventId - k24018_arg0;
+			_isInAnim[shade] = false;
+			levelRender();
+			break;
+		}
+		case 24019:
+			if (persistent->_quest == kRescuePhilQuest) {
+				g_vm->moveToRoom(kMonsterPuzzle);
+			} else {
+				_levelS++;
+				if (_levelS < 15) {
+					levelClear();
+					loadLevel();
+					levelRender();
+					showCharon();
+					break;
+				}
+
+				playCharonSound(Common::String::format("V9280w%c0", 'A' + _levelL), 24020);
+				_levelL++;
+				_levelS = 0;
+			}
+			break;
+		case 24020:
+			if (_levelL == 4)
+				g_vm->moveToRoom(kWallOfFameRoom);
+			break;
+		case k24802_arg0:
+			playCharonSound(charonFinishSounds[g_vm->getRnd().getRandomNumberRng(0, 2)], 24807);
+			break;
+		case k24802_arg1:
+			hideCharon();
+			room->playVideo("V9300bA0", kCharonZ, 24019,
+					Common::Point(406, 68));
+			break;
+		case 24807:
+			g_vm->addTimer(k24802_arg1, 500);
+			break;
+		case 24811:
+			showCharon();
+			break;
+		case kCharonEndTalk:
+			_charonIsBusy = false;
+			break;
+		case 24813: {
+			g_vm->addTimer(24813, g_vm->getRnd().getRandomNumberRng(12000, 18000));
+			if (_charonIsBusy)
+				break;
+			hideCharon();
+			int vid = g_vm->getRnd().getRandomNumberRng(0, ARRAYSIZE(charonIdleVideos) - 1);
+			room->playVideo(charonIdleVideos[vid].name,
+					kCharonZ, 24811, charonIdleVideos[vid].offset);
+			break;
+		}
+		case k24801_arg1:
+			playCharonSoundSMK("V9090NH0", k24801_arg2);
+			_count24006 = 0;
+			g_vm->addTimer(24006, 1200);
+			break;
+		case k24801_arg2:
+			playCharonSoundSMK("V9090ND0", k24801_arg3);
+			break;
+		case k24801_arg3:
+			playCharonSoundSMK("V9090NE0", k24801_arg4);
+			break;
+		case k24801_arg4:
+			playCharonSoundSMK("V9090NI0", k24801_arg5);
+			break;
+		case k24801_arg5:
+			playCharonSoundSMK("V9130NA0", k24801_arg6);
+			break;
+		case k24801_arg6:
+			room->enableMouse();
+			showCharon();
+			break;
+		}
+	}
+
+	void prepareRoom() override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		room->addStaticLayer("V9010pA0", kBackgroundZ);
+		room->selectFrame("V9010oA0", 540, 0);
+		room->selectFrame("V9010oB0", 700, 0);
+		// TODO: semi-transparency
+		room->playAnimLoop("V9060bA0", 540);
+		room->playAnimLoop("V9060bB0", 540);
+		room->playAnimLoop("V9060bC0", 540);
+		room->playVideo("V9010xA0", 0, 24002);
+		room->loadHotZones("ff.hot", false);
+
+		room->playSoundLoop("V9010eA0");
+		room->playSoundLoop("V9300eA0");
+
+		_levelL = 1;
+		if (persistent->_quest == kRescuePhilQuest) {
+			_levelS = 0;
+		} else {
+			_levelS = 1;
+		}
+
+		levelClear();
+		loadLevel();
+		levelRender();
+
+		showCharon();
+		g_vm->addTimer(24813, g_vm->getRnd().getRandomNumberRng(12000, 18000));
+		for (uint i = 0; i < kNumSeats; i++) {
+			g_vm->addTimer(k24017_arg0 + i, g_vm->getRnd().getRandomNumberRng(10200, 21800));
+		}
+
+		g_vm->getHeroBelt()->setColour(HeroBelt::kCold);
+
+		if (persistent->_quest == kRescuePhilQuest) {
+			room->disableMouse();
+			// originally 24800
+			playCharonSoundSMK("V9090NA0", k24801_arg1);
+		}
+	}
+
+	void frameCallback() override {
+		if (_dragged != -1) {
+			levelRender();
+		}
+	}
+
+	bool handleCheat(const Common::String &cheat) override {
+		if (cheat == "done") {
+			win();
+			return true;
+		}
+
+		return false;
+	}
+
+private:
+	void idleAnimShade(int shade, int z, int startFrame, int endFrame) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		LayerId sl(shadows[_shades[shade].shadowId].image, shade, "shadow");
+		_isInAnim[shade] = true;
+		// TODO: play sound multiple times if needed
+		PlayAnimParams params(PlayAnimParams::keepLastFrame().partial(startFrame, endFrame));
+		const char *snd = shadows[_shades[shade].shadowId].animSound;
+		if (snd && snd[0] != 0)
+			room->playAnimWithSound(sl, snd, z, params, EventHandlerWrapper(), getShadowPos(shade));
+		else
+			room->playAnim(sl, z, params, EventHandlerWrapper(), getShadowPos(shade));
+	}
+
+	void hideCharon() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		for (uint i = 0; i < ARRAYSIZE(charonAnims); i++)
+			room->stopAnim(charonAnims[i]);
+		for (uint i = 0; i < ARRAYSIZE(charonIdleVideos); i++)
+			room->stopAnim(charonIdleVideos[i].name);
+		_charonIsBusy = true;
+	}
+
+	void showCharon() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		hideCharon();
+		room->selectFrame(charonAnims[0], kCharonZ, 0);
+		_charonIsBusy = false;
+	}
+
+	void playCharonSound(const Common::String &sound,
+			     EventHandlerWrapper ev = kCharonEndTalk, bool isSMK = false) {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		int selected = -2;
+		hideCharon();
+		do {
+			selected = g_vm->getRnd().getRandomNumberRng(0, ARRAYSIZE(charonAnims) - 1);
+		} while (selected == _lastCharonAnim);
+		_lastCharonAnim = selected;
+		room->playAnim(charonAnims[selected], kCharonZ, PlayAnimParams::loop());
+		if (isSMK)
+			room->playVideo(sound, 0, ev);
+		else
+			room->playSound(sound, ev);
+	}
+
+	void playCharonSoundSMK(const Common::String &sound,
+				EventHandlerWrapper ev = kCharonEndTalk) {
+		playCharonSound(sound, ev, true);
+	}
+
+	void hideThought() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->stopAnim("V9090oA0");
+		for (unsigned i = 0; i < ARRAYSIZE(shadows); i++)
+			room->stopAnim(shadows[i].nameImage);
+		for (unsigned i = 0; i < ARRAYSIZE(thoughts); i++)
+			room->stopAnim(thoughts[i].image);
+		room->stopAnim("V9150tA0");
+	}
+
+	void showThoughtByShadowId(int id, bool silent = false) {
+		Common::Point bubblePos;
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		int thoughtId = _shades[id].thoughtId;
+		if (_shades[id].positionIsFixed && thoughtId == 82)
+			thoughtId = -1;
+		hideThought();
+		bubblePos = getShadowPos(id) - Common::Point(71, 71);
+		if (_shades[id].currentPos >= 0 && _shades[id].currentPos <= 4) {
+			bubblePos += shadows[_shades[id].shadowId].starboardBubble;
+		} else if (_shades[id].currentPos >= 5) {
+			bubblePos += shadows[_shades[id].shadowId].portBubble;
+		} else
+			bubblePos += shadows[_shades[id].shadowId].waitBubble;
+		bubblePos.x = MAX<int>(bubblePos.x, -10);
+		bubblePos.y = MAX<int>(bubblePos.y, 0);
+		if (!silent) {
+			if (thoughtId >= 0)
+				playCharonSound(thoughts[thoughtId].sound);
+			else if (thoughtId == -1)
+				playCharonSound("V9150nA0");
+		}
+		room->selectFrame("V9090oA0", 112, 0, bubblePos);
+		room->selectFrame(shadows[_shades[id].shadowId].nameImage, 111, 0, bubblePos);
+		if (thoughtId >= 0)
+			room->selectFrame(thoughts[thoughtId].image, 111, 0, bubblePos);
+		else if (thoughtId == -1)
+			room->selectFrame("V9150tA0", 111, 0, bubblePos);
+		g_vm->addTimer(24014, 3000);
+	}
+
+	void moveToFerry(int shadeId, int ferryPos) {
+		_shades[shadeId].currentPos = ferryPos;
+
+		// First all other who are disgusted move out
+		for (unsigned i = 0; i < _shades.size(); i++) {
+			if (_shades[i].positionIsFixed || _shades[i].currentPos < 0
+			    || (int) i == shadeId)
+				continue;
+			if (!checkCombinationIsAllowed(i))
+				backToWaiting(i);
+		}
+
+		// Then the new shade
+		if (!checkCombinationIsAllowed(shadeId))
+			backToWaiting(shadeId);
+
+		// Then everybody who is fixed can kick out new shade
+		for (unsigned i = 0; i < _shades.size(); i++) {
+			if (!_shades[i].positionIsFixed)
+				continue;
+			if (!checkCombinationIsAllowed(i))
+				backToWaiting(shadeId);
+		}
+
+		bool isWon = true;
+		for (unsigned i = 0; i < _shades.size(); i++)
+			if (_shades[i].currentPos < 0)
+				isWon = false;
+
+		if (isWon) {
+			win();
+		}
+	}
+
+	void win() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+
+		for (unsigned i = 0; i < ARRAYSIZE(shadows); i++)
+			room->disableHotzone(Common::String::format("s%02d", i));
+		for (unsigned i = 0; i < 2; i++)
+			for (unsigned j = 0; j < 5; j++)
+				room->disableHotzone(Common::String::format("f%01d%01d", i, j));
+		g_vm->addTimer(k24802_arg0, 500);
+	}
+
+	void backToWaiting(int shadeId) {
+		// TODO: anim
+		_shades[shadeId].currentPos = -1;
+	}
+
+	bool isAttribMatch(Shade *shade, int attr) {
+		if (attr == 0)
+			return true;
+		if (!shade)
+			return false;
+		return (shadows[shade->shadowId].attributes & attr) == attr;
+	}
+
+	void yuck() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		if (_isPlayingYuck)
+			return;
+		_isPlayingYuck = true;
+		room->playSound(Common::String::format("V9290n%c0", g_vm->getRnd().getRandomNumberRng('A', 'E')),
+				24010);
+	}
+
+	bool checkCombinationIsAllowed(int shadeId) {
+		Shade *cur = &_shades[shadeId];
+		int curPos = cur->currentPos;
+		int side = curPos / 5;
+		int pos = curPos % 5;
+		Shade *pNextTo1 = pos == 0 ? nullptr : posToShade(curPos-1);
+		Shade *pNextTo2 = pos == 4 ? nullptr : posToShade(curPos+1);
+		Shade *pAfrontOf = posToShade(pos + (side ? 0 : 5));
+		bool pNextTo1Potential = pos != 0 && pNextTo1 == nullptr;
+		bool pNextTo2Potential = pos != 4 && pNextTo2 == nullptr;
+
+		// "Must sit next to"
+		if (!isAttribMatch(pNextTo1, cur->tabN)
+		    && !isAttribMatch(pNextTo2, cur->tabN)
+		    && !pNextTo1Potential && !pNextTo2Potential) {
+			return false;
+		}
+
+		// "Must not sit next to"
+		if (cur->tabNN && (isAttribMatch(pNextTo1, cur->tabNN)
+				   || isAttribMatch(pNextTo2, cur->tabNN))) {
+			yuck();
+			return false;
+		}
+		
+		// "Must sit in front of"
+		if (!isAttribMatch(pAfrontOf, cur->tabA)
+		    && pAfrontOf != nullptr) {
+			return false;
+		}
+
+		// "Must not sit in front of"
+		if (cur->tabNA && isAttribMatch(pAfrontOf, cur->tabNA)) {
+			yuck();
+			return false;
+		}
+
+		// "Must sit in front or back"
+		if ((cur->tabO & 1) && pos != 0 && pos != 4) {
+			return false;
+		}
+
+		// Charon: only X in front
+		if (pos == 4 && !isAttribMatch(cur, _charonN))
+			return false;
+
+		// Charon: no X in front
+		if (pos == 4 && _charonNN && isAttribMatch(cur, _charonNN)) {
+			yuck();
+			return false;
+		}
+
+		// TODO: sound on "must" path
+
+		return true;
+	}
+
+	int typeToIdx(int type) {
+		for (unsigned i = 0; i < _shades.size(); i++) {
+			if (_shades[i].shadowId == type)
+				return i;
+		}
+
+		return -1;
+	}
+
+	Shade *posToShade(int pos) {
+		for (unsigned i = 0; i < _shades.size(); i++) {
+			if (_shades[i].currentPos == pos)
+				return &_shades[i];
+		}
+
+		return nullptr;
+	}
+
+
+	void levelClear() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		for (unsigned i = 0; i < ARRAYSIZE(charonRules); i++) {
+			room->stopAnim(charonRules[i].image);
+		}
+		for (unsigned i = 0; i < _shades.size(); i++) {
+			room->stopAnim(LayerId(shadows[_shades[i].shadowId].image, i, "shadow"));
+			room->stopAnim(shadows[_shades[i].shadowId].nameImage);
+		}
+		room->stopAnim("V9090oA0");
+		for (unsigned i = 0; i < ARRAYSIZE(thoughts); i++) {
+			room->stopAnim(thoughts[i].image);
+		}
+	}
+
+	void levelRender() {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->selectFrame(charonRules[_charonTID].image, 699, 0);
+		room->setHotzoneEnabled("Sign", _charonTID != 12);
+		room->enableHotzone("Charon");
+
+		for (unsigned i = 0; i < ARRAYSIZE(shadows); i++)
+			room->disableHotzone(Common::String::format("s%02d", i));
+
+		bool ffUsed[10];
+
+		memset(ffUsed, 0, sizeof(ffUsed));
+
+		for (uint i = 0; i < _shades.size(); i++) {
+			if ((int) i == _dragged) {
+				Common::Point pos = g_vm->getMousePos();
+				room->selectFrame(LayerId(shadows[_shades[i].shadowId].image,
+							  i, "shadow"), 0, 0, pos - Common::Point(88, 160));
+				_isInAnim[i] = false;
+				continue;
+			}
+
+			if (_isInAnim[i])
+				continue;
+
+			if (_shades[i].currentPos >= 0) {
+				LayerId sl(shadows[_shades[i].shadowId].image,
+					   i, "shadow");
+				int frame = _shades[i].currentPos / 5 ? shadows[_shades[i].shadowId].portFrame
+					: shadows[_shades[i].shadowId].starboardFrame;
+				room->selectFrame(sl, boatPosition[_shades[i].currentPos].z,
+						  frame, getShadowPos(i));
+				ffUsed[_shades[i].currentPos] = true;
+				continue;
+			}
+
+			if (_shades[i].waitingPos >= 0) {
+				Common::Point pos = getShadowPos(i);
+				Common::String hz = Common::String::format("s%02d", _shades[i].shadowId);
+				room->enableHotzone(hz);
+				room->setHotZoneOffset(hz, waitPosition[_shades[i].waitingPos].getPoint()
+						       - Common::Point(88, 160) + Common::Point(0, shadows[_shades[i].shadowId].waityoffset));
+				room->selectFrame(LayerId(shadows[_shades[i].shadowId].image,
+							  i, "shadow"),
+						  waitPosition[_shades[i].waitingPos].z, 0, pos);
+				continue;
+			}
+		}
+
+		for (unsigned i = 0; i < 2; i++)
+			for (unsigned j = 0; j < 5; j++)
+				room->setHotzoneEnabled(Common::String::format("f%01d%01d", i, j),
+							_dragged != -1 ? !ffUsed[i*5 + j] : ffUsed[i*5 + j]);
+	}
+
+	Common::Point getShadowPos(unsigned idx) {
+		if (_shades[idx].currentPos >= 0) {
+			Common::Point delta;
+			if (_shades[idx].shadowId == 14 || _shades[idx].shadowId == 20)
+				delta = Common::Point(0, -35);
+			return boatPosition[_shades[idx].currentPos].getPoint()
+				- Common::Point(88, 160) + delta;
+		}
+
+		if (_shades[idx].waitingPos >= 0) {
+			return waitPosition[_shades[idx].waitingPos].getPoint()
+				- Common::Point(88, 160) + Common::Point(0, shadows[_shades[idx].shadowId].waityoffset);
+		}
+
+		return Common::Point(0, 0);
+	}
+
+	void loadLevel() {
+		TextTable _levelTable;
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		_levelTable = TextTable(
+			Common::SharedPtr<Common::SeekableReadStream>(
+				room->openFile(
+					Common::String::format("l%ds%02d.ff", _levelL, _levelS))),
+			14);
+		_shades.clear();
+		for (int i = 0; i < _levelTable.size(); i++) {
+			if (_levelTable.get(i, "Num") == "") {
+				_charonTID = _levelTable.get(i, "TID").asUint64Ext();
+				_charonN = _levelTable.get(i, "N").asUint64Ext();
+				_charonNN = _levelTable.get(i, "NN").asUint64Ext();
+				continue;
+			}
+
+			Shade shade;
+			shade.shadowId = _levelTable.get(i, "Num").asUint64Ext();
+			shade.thoughtId = _levelTable.get(i, "TID").asUint64Ext();
+			if (_levelTable.get(i, "BoatPos") == "" || _levelTable.get(i, "BoatPos") == "0") {
+				shade.currentPos = -1;
+				shade.positionIsFixed = false;
+			} else {
+				shade.currentPos = _levelTable.get(i, "BoatPos").asUint64Ext() - 1;
+				shade.positionIsFixed = true;
+			}
+			shade.tabNA = _levelTable.get(i, "NA").asUint64Ext();
+			shade.tabNN = _levelTable.get(i, "NN").asUint64Ext();
+			shade.tabN = _levelTable.get(i, "N").asUint64Ext();
+			shade.tabO = _levelTable.get(i, "O").asUint64Ext();
+			shade.tabA = _levelTable.get(i, "A").asUint64Ext();
+			shade.waitingPos = -1;
+			shade.rowidx = _shades.size();
+			_shades.push_back(shade);
+		}
+
+		for (int j = 0; j < 2; j++) {
+			int m = 100000, mpos = -1;
+			for (unsigned i = j; i < _shades.size(); i++) {
+				if (shadows[_shades[i].shadowId].priorityleft <= m && !_shades[i].positionIsFixed) {
+					m = shadows[_shades[i].shadowId].priorityleft;
+					mpos = i;
+				}
+			}
+			if (shadows[_shades[j].shadowId].priorityleft <= m && !_shades[j].positionIsFixed)
+				continue;
+			Shade t = _shades[mpos];
+			_shades[mpos] = _shades[j];
+			_shades[j] = t;
+		}
+
+		for (int j = 0; j < 2; j++) {
+			int m = 100000, mpos = -1;
+			for (unsigned i = 2; i < _shades.size() - j; i++) {
+				if (shadows[_shades[i].shadowId].priorityright <= m && !_shades[i].positionIsFixed) {
+					m = shadows[_shades[i].shadowId].priorityright;
+					mpos = i;
+				}
+			}
+			if (shadows[_shades[_shades.size() - j - 1].shadowId].priorityright <= m
+				&& !_shades[_shades.size() - j - 1].positionIsFixed)
+				continue;
+			Shade t = _shades[mpos];
+			_shades[mpos] = _shades[_shades.size() - j - 1];
+			_shades[_shades.size() - j - 1] = t;
+		}
+
+		int waitidx = 0;
+
+		for (unsigned i = 0; i < _shades.size(); i++) {
+			if (!_shades[i].positionIsFixed)
+				_shades[i].waitingPos = waitidx;
+			debug("%s, leftpriority %d, rightpriority %d, idx %d, res %d",
+			      shadows[_shades[i].shadowId].name,
+			      shadows[_shades[i].shadowId].priorityleft,
+			      shadows[_shades[i].shadowId].priorityright,
+			      _shades[i].rowidx,
+			      _shades[i].waitingPos);
+			waitidx++;
+		}
+	}
+	int _levelL;
+	int _levelS;
+	int _charonTID;
+	int _charonN;
+	int _charonNN;
+	int _dragged;
+	int _clickTimer;
+	int _lastCharonAnim;
+	int _count24006;
+	bool _isPlayingYuck;
+	bool _charonIsBusy;
+	Common::Array<Shade> _shades;
+	bool _isInAnim[kNumSeats];
+};
+
+Common::SharedPtr<Hadesch::Handler> makeFerryHandler() {
+	return Common::SharedPtr<Hadesch::Handler>(new FerryHandler());
+}
+
+}
diff --git a/engines/hadesch/rooms/hadesthrone.cpp b/engines/hadesch/rooms/hadesthrone.cpp
new file mode 100644
index 0000000000..731678f1ad
--- /dev/null
+++ b/engines/hadesch/rooms/hadesthrone.cpp
@@ -0,0 +1,64 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+
+namespace Hadesch {
+
+class HadesThroneHandler : public Handler {
+public:
+	HadesThroneHandler() {
+	}
+
+	void handleClick(const Common::String &name) override {
+	}
+
+	void handleEvent(int eventId) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+
+		switch(eventId) {
+		case 29001:
+			persistent->_quest = kEndGame;
+			persistent->clearInventory();
+			persistent->_doQuestIntro = true;
+			g_vm->moveToRoom(kWallOfFameRoom);
+			break;
+		}
+	}
+
+	void prepareRoom() override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->playVideo("movie", 500, 29001);
+		room->disableHeroBelt();
+		room->playSoundLoop("V6010eA0");
+		room->disableMouse();
+	}
+};
+
+Common::SharedPtr<Hadesch::Handler> makeHadesThroneHandler() {
+	return Common::SharedPtr<Hadesch::Handler>(new HadesThroneHandler());
+}
+
+}
diff --git a/engines/hadesch/rooms/intro.cpp b/engines/hadesch/rooms/intro.cpp
new file mode 100644
index 0000000000..9e6eb44e61
--- /dev/null
+++ b/engines/hadesch/rooms/intro.cpp
@@ -0,0 +1,60 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+
+namespace Hadesch {
+
+class IntroHandler : public Handler {
+public:
+	IntroHandler() {
+	}
+
+	void handleClick(const Common::String &name) override {
+	}
+
+	void handleEvent(int eventId) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+
+		switch(eventId) {
+		  // 32002 handles performance testing
+		case 32003:
+			g_vm->moveToRoom(kOlympusRoom);
+			break;
+		}
+	}
+
+	void prepareRoom() override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		room->playVideo("o0010ba0", 101, 32003);
+		room->disableHeroBelt();
+		room->disableMouse();
+	}
+};
+
+Common::SharedPtr<Hadesch::Handler> makeIntroHandler() {
+	return Common::SharedPtr<Hadesch::Handler>(new IntroHandler());
+}
+
+}
diff --git a/engines/hadesch/rooms/medisle.cpp b/engines/hadesch/rooms/medisle.cpp
new file mode 100644
index 0000000000..50e6edf7d3
--- /dev/null
+++ b/engines/hadesch/rooms/medisle.cpp
@@ -0,0 +1,1284 @@
+/* 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.
+ *
+ * Copyright 2020 Google
+ *
+ */
+#include "hadesch/hadesch.h"
+#include "hadesch/video.h"
+#include "hadesch/ambient.h"
+
+namespace Hadesch {
+
+static const char *kStoneAnim = "g0110ob0";
+static const char *kStoneHotzone = "stone";
+static const char *snakes[] = {
+	"m1220bb0",
+	"m1210bb0",
+	"m1190bb0",
+	"m1180bb0"
+};
+
+static const char *greenSnakes[] = {
+	"m1160bd0",
+	"m1160bc0",
+	"m1160bb0",
+	"m1160ba0"
+};
+
+static const char *itemImages[] = {
+	"m1010bb0",
+	"m1010bc0",
+	"m1010ba0",
+	"m1010bd0",
+	"m1010be0"
+};
+
+static const char *itemImagesGlow[] = {
+	"m1010bb1",
+	"m1010bc1",
+	"m1010ba1",
+	"m1010bd1",
+	"m1010be1"
+};
+
+static const char *itemSounds[] = {
+	"m1190ea0",
+	"m1180ec0",
+	"m1220ea0",
+	"m1210ea0",
+	"m1230ea0"
+};
+
+static const char *itemClickSounds[] = {
+	"m1150ne0",
+	"m1150nd0",
+	"m1150na0",
+	"m1150nb0",
+	"m1150nc0"
+};
+
+static const char *perseusItemAnims[] = {
+	"m1190ba0",
+	"m1180ba0",
+	"m1220ba0",
+	"m1210ba0",
+	"m1230ba0"
+};
+
+static const char *perseusItemSounds[] = {
+	"m1190na0",
+	"m1180na0",
+	"m1220na0",
+	"m1210na0",
+	"m1230na0"
+};
+
+static const int kBagPuzzleNumElements = 10;
+
+static const char *statueFullElements[kBagPuzzleNumElements] = {
+	  "m1010or0",
+	  "m1010or1",
+	  "m1010ov0",
+	  "m1010ow0",
+	  "m1010os0",
+	  "m1010ot0",
+	  "m1010ou0",
+	  "m1010op0",
+	  "m1010oq0",
+	  "m1010oo0"
+};
+
+static const char *statueEmptyElements[kBagPuzzleNumElements] = {
+	  "m1010or2",
+	  "m1010or3",
+	  "m1010ov1",
+	  "m1010ow1",
+	  "m1010os1",
+	  "m1010ot1",
+	  "m1010ou1",
+	  "m1010op1",
+	  "m1010oq1",
+	  "m1010oo1"
+};
+
+static const int statuesZvals1[kBagPuzzleNumElements] = {
+	2009,
+	2008,
+	2010,
+	2006,
+	2007,
+	2005,
+	2004,
+	2002,
+	2001,
+	2003
+};
+
+static Common::Point statuesOffsets1[kBagPuzzleNumElements] = {
+	Common::Point(519, 282),
+	Common::Point(571, 276),
+	Common::Point(528, 225),
+	Common::Point(547, 225),
+	Common::Point(518, 159),
+	Common::Point(500, 161),
+	Common::Point(563, 173),
+	Common::Point(471, 132),
+	Common::Point(565, 193),
+	Common::Point(539, 107)
+};
+
+static Common::Point statueBrokenPiecesOffsets[kBagPuzzleNumElements] = {
+	Common::Point(461, 395),
+	Common::Point(339, 358),
+	Common::Point(590, 356),
+	Common::Point(298, 415),
+	Common::Point(517, 407),
+	Common::Point(582, 453),
+	Common::Point(381, 362),
+	Common::Point(483, 357),
+	Common::Point(426, 360),
+	Common::Point(406, 307)
+};
+
+static Common::Point statuePieceHotspot[kBagPuzzleNumElements] = {
+	Common::Point(26, 28),
+	Common::Point(15, 30),
+	Common::Point(11, 24),
+	Common::Point(15, 25),
+	Common::Point(23, 26),
+	Common::Point(13, 10),
+	Common::Point(10, 13),
+	Common::Point(20, 24),
+	Common::Point(25, 35),
+	Common::Point(24, 32)
+};
+
+static const char *statuePieceNames[kBagPuzzleNumElements] = {
+	"LowerLeg1",
+	"LowerLeg2",
+	"UpperLeg1",
+	"UpperLeg2",
+	"Torso",
+	"UpperArm1",
+	"UpperArm2",
+	"LowerArm1",
+	"LowerArm2",
+	"Head"
+};
+
+static const char *fatesHotzoneNames[kNumFates] = {
+	"Lachesis",
+	"Atropos",
+	"Clotho"
+};
+
+static const char *itemNames[] = {
+	"shield",
+	"sword",
+	"bag",
+	"helmet",
+	"sandals"
+};
+
+static const int statuePiecesDepMap[kBagPuzzleNumElements][2] = {
+	{ -1, -1 },
+	{ -1, -1 },
+	{  0, -1 },
+	{  1, -1 },
+	{  2,  3 },
+	{  4, -1 },
+	{  4, -1 },
+	{  5, -1 },
+	{  6, -1 },
+	{  4, -1 }
+};
+
+static const struct {
+	const char *image;
+	int minint, maxint;
+	int zVal;
+} ambientsLeft[] = {
+	{ "m1030ba0", 10000, 40000, 4000 },
+	{ "m1320ba0", 15000, 30000, 4000 },
+	{ "m1320bb0", 10000, 50000, 4000 },
+	{ "m1320bc0", 5000, 20000, 4000 },
+	{ "m1330ba0", 10000, 40000, 4000 },
+	{ "m1330bb0", 5000, 20000, 4000 },
+	{ "m1330bc0", 5000, 30000, 4000 },
+	{ "m1340ba0", 5000, 20000, 4000 },
+	{ "m1340bb0", 15000, 30000, 4000 },
+	{ "m1340bc0", 5000, 20000, 4000 }
+};
+
+static const struct {
+	const char *image;
+	const char *sound;
+	int minint, maxint;
+	int zVal;
+	int parallax;
+	bool keep;
+} ambientsLeftSnakesAndRats[] = {
+	{ "m1090ba0", "m1090ea0", 5000, 45000, 300, -200, true },
+	{ "m1310ba0", "m1310ea0", 5000, 45000, 300, -200, true },
+	{ "m1100ba0", "m1100ea0", 5000, 45000, 4100, 0, true },
+	{ "m1300ba0", "m1300ea0", 5000, 45000, 4100, 0, true },
+	{ "m1060ba0", "m1060ea0", 5000, 50000, 4200, 0, false },
+	{ "m1280ba0", "m1280ea0", 5000, 50000, 1500, -200, false }
+};
+
+static const struct {
+	const char *image;
+	int minint, maxint;
+	int zVal;
+} ambientsRight[] = {
+	{"m2060ba0", 5000, 40000, 4000},
+	{"m2060bb0", 10000, 30000, 4000},
+	{"m2280ba0", 5000, 20000, 2000},
+	{"m2270ba0", 3000, 20000, 4000}
+};
+
+enum {
+	kStoneTakenCleanup = 11050,
+	kStoneTaken = 1011050
+};
+
+enum {
+	kBackgroundZ = 10000,
+	kStatuesZVal2 = 3000,
+	kFatesZ = 1500
+};
+
+enum {
+	kLoopFatesShadow = 1011001
+};
+
+class MedIsleHandler : public Handler {
+public:
+	MedIsleHandler() {
+		_eyeInsistCounter = 0;
+		_eyeIsGivenBack = false;
+		_eyeIsPickedUp = false;
+		_fatesShadowIsActive = false;
+		_isFirstFates = false;
+		_statueDrag = -1;
+		_depProblemState = 0;
+		_fatesAreBusy = false;
+		_lastClickedItem = -1;
+		_hintsCounter = 0;
+	}
+
+	void handleClick(const Common::String &name) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		Quest quest = persistent->_quest;
+
+		for (int i = 0; i < kBagPuzzleNumElements; i++) {
+			if (name == Common::String("D") + statuePieceNames[i]) {
+				statueDClick(i);
+				return;
+			}
+			if (name == Common::String("S") + statuePieceNames[i]) {
+				statueSClick(i);
+				return;
+			}
+		}
+
+		if (_statueDrag >= 0) {
+			return;
+		}
+		if (name == kStoneHotzone) {
+			room->stopAnim(kStoneAnim);
+			g_vm->getHeroBelt()->placeToInventory(kStone, kStoneTaken);
+			room->disableHotzone(kStoneHotzone);
+			room->playSound("m1360ma0");
+			persistent->_medisleStoneTaken = true;
+			room->disableMouse();
+			return;
+		}
+
+		if (name == "Argo") {
+			g_vm->moveToRoom(kArgoRoom);
+			return;
+		}
+
+		if (name == "Eyeball") {
+			room->drag("m2010oa0", 0, Common::Point(21, 20));
+			room->stopAnim("m2010oa0");
+			room->disableMouse();
+			room->playSound("m2130ea0", 11027);
+			_eyeIsPickedUp = true;
+			return;
+		}
+
+		if (_eyeIsGivenBack) {
+			static const int nextEvent[] = {
+				11621, 11623, 11626
+			};
+			for (FateId i = kLachesis; i < kNumFates; i = (FateId) (i + 1)) {
+				if (name == fatesHotzoneNames[i]) {
+					moveEye(i, nextEvent[i]);
+					return;
+				}
+			}
+		}
+
+		if (_eyeIsPickedUp && !_eyeIsGivenBack) {
+			for (FateId i = kLachesis; i < kNumFates; i = (FateId) (i + 1))
+				if (name == fatesHotzoneNames[i]) {
+					persistent->_medisleEyePosition = i;
+					room->disableMouse();
+					room->playSound("m2130ee0", 11029);
+					room->disableHotzone("Eyeball");
+					room->clearDrag();
+					_eyeIsGivenBack = true;
+					_isFirstFates = true;
+					return;
+				}
+		}
+
+		if (name == "MagicBag") {
+			hideMagicBag();
+			persistent->_medisleBagPuzzleState = Persistent::BAG_TAKEN;
+			g_vm->getHeroBelt()->placeToInventory(kBag);
+			renderFatesAll();
+			return;
+		}
+
+		for (int i = 0; i < 5; i++) {
+			if (name == itemNames[i]) {
+				itemGlow(i);
+				return;
+			}
+		}
+		
+		if (name == "FatesLair") {
+			if (showAllFates())
+				return;
+			room->disableMouse();
+			if (showNoFates()) {
+				_hintsCounter++;
+				if (_hintsCounter == 1 && persistent->_medislePlayedPhilFatesDesc)
+					_hintsCounter = 2;
+				if (_hintsCounter == 1) {
+					if (quest > kMedusaQuest || (quest == kMedusaQuest && persistent->_medisleShowFates)) {
+						playFatesLairBackupSound();
+						return;
+					}
+					persistent->_medislePlayedPhilFatesDesc = true;
+					room->playVideo("m2210ba0", 0, 11049, Common::Point(640, 216));
+					return;
+				}
+
+				Common::Array <Common::String> hints;
+
+				switch (quest) {
+				case kCreteQuest:
+					hints.push_back("m2220wa0");
+					hints.push_back("m2220wb0");
+					hints.push_back("m2220wc0");
+					break;
+				case kTroyQuest:
+					hints.push_back("m2230wa0");
+					hints.push_back("m2230wb0");
+					hints.push_back("m2230wc0");
+					break;
+				case kMedusaQuest:
+					if (persistent->_medisleShowFates) {
+						hints.push_back("m2210wa0");
+						hints.push_back("m2250wa0");
+						hints.push_back("m2250wb0");
+					}
+					break;
+				case kRescuePhilQuest:
+					hints.push_back("m2240wa0");
+					hints.push_back("m2240wb0");
+					break;
+				// To silence warning
+				case kNoQuest:
+				case kEndGame:
+				case kNumQuests:
+					break;
+				}
+
+				if (hints.empty()) {
+					playFatesLairBackupSound();
+				} else {
+					fatesShadowSound(hints[(_hintsCounter - 2) % hints.size()], 11632);
+				}
+				return;
+			}
+
+			if (showFate(kLachesis) && showFate(kAtropos) && !showFate(kClotho)) {
+				fatesShadowSound("m2210wd0", 11632);
+			}
+
+			if (showFate(kLachesis) && !showFate(kAtropos) && showFate(kClotho)) {
+				fatesShadowSound("m2210wa0", 11632);
+			}
+
+			if (showFate(kLachesis) && !showFate(kAtropos) && !showFate(kClotho)) {
+				fatesShadowSound("m2190wd0", 11632);
+			}
+
+			if (!showFate(kLachesis) && showFate(kAtropos) && showFate(kClotho)) {
+				fatesShadowSound("m2190wa0", 11632);
+			}
+
+			if (!showFate(kLachesis) && showFate(kAtropos) && !showFate(kClotho)) {
+				fatesShadowSound(_hintsCounter & 1 ? "m2190wa0" : "m2190wd0", 11632);
+			}
+
+			if (!showFate(kLachesis) && !showFate(kAtropos) && showFate(kClotho)) {
+				switch (_hintsCounter % 3) {
+				case 0:
+					fatesShadowSound("m2190wb0", 11629);
+					return;
+				case 1:
+					fatesShadowSound("m2190wa0", 11632);
+					return;
+				case 2:
+					fatesShadowSound("m2210wa0", 11632);
+					return;
+				}
+			}
+
+			return;
+		}
+
+	  		/*
+TODO (medusa quest):
+ MNSH: Perseus
+ MNSH: MedusasLair
+*/
+	}
+
+  	bool handleClickWithItem(const Common::String &name, InventoryItem item) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+
+		for (int i = 0; i < 5; i++) {
+			if (name == itemNames[i] && item == kShield + i) {
+				itemPlaced(item);
+				return true;
+			}
+		}
+
+		if (name == "Perseus" && (
+			    item >= kShield && item <= kSandals)) {
+			room->disableMouse();
+			playPerseusAnim("m1240ba0", "m1240na0", 11053);
+		}
+
+		return false;
+	}
+
+	void handleEvent(int eventId) override {
+		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
+		Persistent *persistent = g_vm->getPersistent();
+		switch (eventId) {
+		case 11002:
+			if (!room->isMouseEnabled() || !room->isPanRight())
+				break;
+			room->disableMouse();
+			room->playVideo("m2100ba0", 0, 11003,
+					Common::Point(640, 216));
+			break;
+		case 11003:
+			room->playSound("m2100wa0", 11004);
+			room->enableMouse();
+			break;
+		case kStoneTaken:
+			room->playVideo("m1360ba0", 200, kStoneTakenCleanup, Common::Point(0, 216));
+			break;
+		case 11064: // Right pan
+			if (persistent->_medisleShowFatesIntro) {
+				persistent->_medisleShowFatesIntro = false;
+				persistent->_medisleShowFates = true;
+				room->setPannable(false);
+				room->disableHotzone("Argo");
+				room->playSound("m2120ma0", 11642);
+				g_vm->addTimer(11601, 6500);
+				room->disableMouse();
+				persistent->_medisleEyeballIsActive = true;
+			}
+			break;
+		case 11601:
+			fatesShadowSound("m2120wa0", 11602);
+			break;
+		case 11602:
+			fatesShadowSound("m2120wb0", 11603);
+			break;
+		case 11603:
+			fatesShadowSound("m2120wc0", 11604);
+			break;
+		case 11604:
+			fatesShadowSoundEnd();
+			room->playAnimWithSound("m2120ba0", "m2120ea0", 280, PlayAnimParams::disappear(), 11605, kOffsetRightRoom);
+			break;
+		case 11605:
+			room->enableMouse();
+			room->enableHotzone("Eyeball");
+			room->selectFrame("m2010oa0", 280, 0, Common::Point(949, 409));
+			room->setLayerParallax("m2010oa0", -200);
+			g_vm->addTimer(11606, g_vm->getRnd().getRandomNumberRng(10000, 20000), -1);
+			break;
+		case 11606:
+			if (_eyeIsGivenBack || _eyeIsPickedUp)
+				break;
+			fatesShadowSound(Common::String::format("m2120w%c0", 'd' + (_eyeInsistCounter % 3)), 11608);
+			_eyeInsistCounter++;
+			break;
+		case 11608:
+			fatesShadowSoundEnd();
+			break;
+		case 11065: // Left pan
+			if (persistent->_seriphosPlayedMedusa && !persistent->_medislePlayedPerseusIntro) {
+				room->disableMouse();
+				room->playVideo("m1140ba0", 0, 11005, Common::Point(0, 216));
+			}
+			break;
+		case 11005:
+			playPerseusAnim("m1140bb0", "m1140nb0", 11007);
+			persistent->_medislePlayedPerseusIntro = true;
+			break;
+		case 11006:
+			finishPerseusAnim();
+			room->enableMouse();
+			break;
+		case 11007:
+			playPerseusAnim("m1140bc0", "m1140nc0", 11008);
+			break;
+		case 11008:
+			playPerseusAnim("m1140bd0", "m1140nd0", 11006);
+			break;
+		case 11009:
+			room->playSound(itemClickSounds[_lastClickedItem], 11010);
+			break;
+		case 11010:
+			for (int i = 0; i < 5; i++)
+				room->stopAnim(itemImagesGlow[i]);
+			break;
+		case 11012:
+			room->playSound("m1210ma0", 11013);
+			break;
+		case 11013:
+			playPerseusAnim(perseusItemAnims[_lastPlacedItem - kShield],
+					 perseusItemSounds[_lastPlacedItem - kShield], 11014);
+			break;
+		case 11014: {
+			finishPerseusAnim();
+			int event = -1;
+			switch (getNumberOfBroughtItems()) {
+			case 1:
+				event = 11015;
+				break;
+			case 2:
+				persistent->_medisleShowFatesIntro = true;
+				event = 11017;
+				break;
+			case 3:
+				event = 11020;
+				break;
+			case 4:
+				event = 11024;
+				break;
+			case 5:
+				room->playAnimWithSound("m1170ba0",
+							"m1190ec1",
+							806,
+							PlayAnimParams::disappear(),
+							11021);
+				return;
+			}
+			int snakeIdx = 4 - getNumberOfBroughtItems();
+			room->playAnimWithSound(snakes[snakeIdx],
+						"m1190ec1", 807 + snakeIdx,
+						PlayAnimParams::disappear(),
+						event);
+		}
+			break;
+		case 11015:
+			renderPerseus();
+			if (_lastPlacedItem == kSword)
+				playPerseusAnim("m1180bc0", "m1180nb0", 11016);
+			else
+				playPerseusAnim("m1190bc0", "m1190nb0", 11016);
+			break;
+		case 11016:
+		case 11025:
+		case 11053:
+			finishPerseusAnim();
+			room->enableMouse();
+			break;
+		case 11017:
+			renderPerseus();
+			playPerseusAnim("m1200ba0", "m1200na0", 11018);
+			break;
+		case 11018:
+			finishPerseusAnim();
+			room->playVideo("m1200ma0", 0, 11019);
+			break;
+		case 11019:
+		case 11037:
+		case 11042:
+		case 11049:
+		case kStoneTakenCleanup:
+			room->enableMouse();
+			break;
+		case 11021:
+			for (int i = 0; i < 5; i++)
+				room->stopAnim(itemImages[i]);
+			for (int i = 0; i < 4; i++)
+				room->stopAnim(snakes[i]);
+			for (int i = 0; i < 4; i++)
+				room->stopAnim(greenSnakes[i]);
+			_perseusAnim.hide();
+			room->stopAnim("m1010oi0");
+			room->playVideo("m1260bh0", 900, 11023, Common::Point(2, 60));
+			break;
+		case 11023:
+			room->selectFrame("m1260bh1", 900, 0);
+			// TODO: arcade sequence
+			if (0) {
+				g_vm->moveToRoom(kMedusaPuzzle);
+			} else
+				g_vm->moveToRoom(kQuiz);
+			break;
+		case 11024:
+			renderPerseus();
+			playPerseusAnim("m1250bb0", "m1250nb0", 11025);
+			break;
+		case 11020:
+			renderPerseus();
+			playPerseusAnim("m1250ba0", "m1250na0", 11025);
+			break;
+		case 11027:
+			room->playVideo("m2130ba0", kFatesZ, 11609, Common::Point(756, 0));
+			break;
+		case 11029:
+			moveEye(kLachesis, 11621);
+			break;
+		case 11621:
+			renderFatesExcept(kLachesis);
+			room->playVideo("m2160ba0", kFatesZ, _isFirstFates ? 11622 : 11627, Common::Point(854, 0));
+			break;
+		case 11622:
+			moveEye(kAtropos, 11623);
+			break;
+		case 11623:
+			renderFatesExcept(kAtropos);
+			room->playVideo("m2170ba0", kFatesZ, _isFirstFates ? 11624 : 11627, Common::Point(1002, 96));
+			break;
+		case 11624:
+			if (persistent->_medisleBagPuzzleState < Persistent::BAG_STARTED)
+				persistent->_medisleBagPuzzleState = Persistent::BAG_STARTED;
+			room->playSound("m2200ea0");
+			startBagPuzzle();
+			// Fallthrough
+		case 11625:
+			moveEye(kClotho, 11626);
+			break;
+		case 11626:
+			renderFatesExcept(kClotho);
+			room->playVideo("m2180ba0", kFatesZ, 11627, Common::Point(1090, 68));
+			break;
+		case 11627:
+			renderFatesAll();
+			room->enableMouse();
+			_isFirstFates = false;
+			room->setPannable(true);
+			room->enableHotzone("Argo");
+			break;
+		case 11044:
+			if (room->isMouseEnabled() && !_eyeIsGivenBack) {
+				room->playVideo("m2130bd0", 0, 11045, Common::Point(640, 216));
+			}
+			break;
+		case 11609:
+			renderFatesExcept(kAtropos, kClotho);
+			room->playVideo("m2130bb0", kFatesZ, 11610, Common::Point(922, 0));
+			break;
+		case 11610:
+			renderFatesExcept(kClotho);
+			room->playVideo("m2130bc0", kFatesZ, 11611, Common::Point(1024, 0));
+			break;
+		case 11611:
+			renderFatesAll();
+			room->enableMouse();
+			g_vm->addTimer(11044, 5000);
+			g_vm->addTimer(11615, g_vm->getRnd().getRandomNumberRng(5000, 10000), -1);
+			break;
+		case 11615: {
+			if (showNoFates() || _fatesAreBusy || !room->isPanRight())
+				break;
+			_fatesAreBusy = true;
+
+			FateId fate = (FateId) g_vm->getRnd().getRandomNumberRng(0, 2);
+
+			if (!showFate(fate) && fate == kLachesis) {
+				room->playSound(
+					g_vm->getRnd().getRandomNumberRng(0, 1) ? "m2160wb0" : "m2160wa0", 11616);
+				break;
+			}
+
+			if (!showFate(fate)) {
+				_fatesAreBusy = false;
+				break;
+			}
+
+			// Move eye to another fate
+			if (persistent->_medisleEyePosition == fate) {
+				int variants = 0;
+				for (FateId i = kLachesis; i < kNumFates; i = (FateId) (i + 1))
+					if (showFate(i) && i != fate)
+						variants++;
+				if (variants <= 0) {
+					_fatesAreBusy = false;
+					break;
+				}
+
+				int off = variants == 1 ? 0 : g_vm->getRnd().getRandomNumberRng(0, 1);
+
+				FateId moveTo = kLachesis;
+				int j = 0;
+
+				for (FateId i = kLachesis; i < kNumFates; i = (FateId) (i + 1))
+					if (showFate(i) && i != fate) {
+						if (j == off) {
+							moveTo = i;
+							break;
+						}
+						j++;
+					}
+
+				moveEye(moveTo, 11617);
+				break;
+			}
+
+			switch (fate) {
+			case kLachesis:
+				room->stopAnim("m2140od0");
+				if (g_vm->getRnd().getRandomNumberRng(0, 1)) {
+					room->playAnimWithSound("m2140ba0", "m2140ea0", 1500,
+								PlayAnimParams::disappear(), 11617, kOffsetRightRoom);
+				} else {
+					room->playAnimWithSound("m2140be0", "m2140ee0", 1500,
+								PlayAnimParams::disappear(), 11617, kOffsetRightRoom);
+				}
+				break;
+			case kAtropos:
+				room->stopAnim("m2140oe0");
+				room->playAnimWithSound("m2140bg0", "m2140eg0", 1500,
+							PlayAnimParams::disappear(), 11617, kOffsetRightRoom);
+				break;
+			case kClotho:
+				room->stopAnim("m2140of0");
+				room->playAnimWithSound("m2140bh0", "m2140eh0", 1500,
+							PlayAnimParams::disappear(), 11617, kOffsetRightRoom);
+				break;
+			// To silence warning
+			case kNumFates:
+				break;
+			}
+			break;
+		}
+		case 11616:
+		case 11617:
+			renderFatesAll();
+			break;
+		case 11629:
+			room->playSound("m2190wc0", 11630);
+			break;
+		case 11630:
+		case 11632:
+			room->enableMouse();
+			fatesShadowSoundEnd();
+			break;
+		case kLoopFatesShadow:
+			room->playAnim("m2280bc0", 4000,
+				       PlayAnimParams::loop().partial(10, 49), -1, kOffsetRightRoom);
+			break;
+		case 11035:
+			room->playSound("m1270ea0", 11203);
+			_statueDrag = -1;
+			renderStatue();
+			break;
+		case 11203:
+			if (isAllPlaced()) {
+				room->playAnimWithSound("m1270bc0", "m1270eb0", 500, PlayAnimParams::disappear(), 11038);
+				room->disableMouse();
+			}
+			break;
+		case 11038:
+			persistent->_medisleBagPuzzleState = Persistent::BAG_SOLVED;
+			showMagicBag();




More information about the Scummvm-git-logs mailing list