[Scummvm-git-logs] scummvm master -> 0d527c60b8029770ffccdae3631efa7eff19b5dc

bluegr noreply at scummvm.org
Sun Jun 14 10:00:42 UTC 2026


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

Summary:
d4a8400a96 MOHAWK: LB - Cleanup ~4000 LoC in main LB (I).
7d5666a9f5 MOHAWK: LB - move tryOpenPage function to header
0d527c60b8 MOHAWK: LB - Cleanup ~4000 LoC in main LB (II).


Commit: d4a8400a9655b25b5d0472fd47cefd845b0e45b8
    https://github.com/scummvm/scummvm/commit/d4a8400a9655b25b5d0472fd47cefd845b0e45b8
Author: Alstruit (34786806+Alstruit at users.noreply.github.com)
Date: 2026-06-14T13:00:37+03:00

Commit Message:
MOHAWK: LB - Cleanup ~4000 LoC in main LB (I).

Move bookinfo and UI menu related into their respective files.
Move LBAnimationNode into its own file.
Move LBPage, BITL into livingbooks_page.cpp
Create livingbooks_itemtypes.cpp to handle misc item types.

Changed paths:
  A engines/mohawk/livingbooks_animation.cpp
  A engines/mohawk/livingbooks_bookinfo.cpp
  A engines/mohawk/livingbooks_itemtypes.cpp
  A engines/mohawk/livingbooks_menus.cpp
  A engines/mohawk/livingbooks_page.cpp
    engines/mohawk/livingbooks.cpp
    engines/mohawk/module.mk


diff --git a/engines/mohawk/livingbooks.cpp b/engines/mohawk/livingbooks.cpp
index 6bf80dfba0a..fdcd4f1e2bd 100644
--- a/engines/mohawk/livingbooks.cpp
+++ b/engines/mohawk/livingbooks.cpp
@@ -75,62 +75,6 @@ Common::Rect MohawkEngine_LivingBooks::readRect(Common::ReadStreamEndian *stream
 	return rect;
 }
 
-LBPage::LBPage(MohawkEngine_LivingBooks *vm) : _vm(vm) {
-	_code = nullptr;
-	_mhk = nullptr;
-
-	_baseId = 0;
-	_cascade = false;
-}
-
-void LBPage::open(Archive *mhk, uint16 baseId) {
-	_mhk = mhk;
-	_baseId = baseId;
-
-	_vm->addArchive(_mhk);
-	if (!_vm->hasResource(ID_BCOD, baseId)) {
-		// assume that BCOD is mandatory for v4/v5
-		if (_vm->getGameType() == GType_LIVINGBOOKSV4 || _vm->getGameType() == GType_LIVINGBOOKSV5)
-			error("missing BCOD resource (id %d)", baseId);
-		_code = new LBCode(_vm, 0);
-	} else {
-		_code = new LBCode(_vm, baseId);
-	}
-
-	loadBITL(baseId);
-	for (uint i = 0; i < _items.size(); i++)
-		_vm->addItem(_items[i]);
-
-	for (uint32 i = 0; i < _items.size(); i++)
-		_items[i]->init();
-
-	for (uint32 i = 0; i < _items.size(); i++)
-		_items[i]->startPhase(kLBPhaseLoad);
-}
-
-void LBPage::addClonedItem(LBItem *item) {
-	_vm->addItem(item);
-	_items.push_back(item);
-}
-
-void LBPage::itemDestroyed(LBItem *item) {
-	for (uint i = 0; i < _items.size(); i++)
-		if (item == _items[i]) {
-			_items.remove_at(i);
-			return;
-		}
-	error("itemDestroyed didn't find item");
-}
-
-LBPage::~LBPage() {
-	delete _code;
-	_vm->removeItems(_items);
-	for (uint i = 0; i < _items.size(); i++)
-		delete _items[i];
-	_vm->removeArchive(_mhk);
-	delete _mhk;
-}
-
 MohawkEngine_LivingBooks::MohawkEngine_LivingBooks(OSystem *syst, const MohawkGameDescription *gamedesc) : MohawkEngine(syst, gamedesc) {
 	DebugMan.addDebugChannel(kDebugCode, "Code", "Track Script Execution");
 
@@ -305,65 +249,6 @@ void MohawkEngine_LivingBooks::pauseEngineIntern(bool pause) {
 	}
 }
 
-void MohawkEngine_LivingBooks::loadBookInfo(const Common::Path &filename) {
-	_bookInfoFile.allowNonEnglishCharacters();
-	if (!_bookInfoFile.loadFromFile(filename))
-		error("Could not open %s as a config file", filename.toString().c_str());
-
-	_title = getStringFromConfig("BookInfo", "title");
-	_copyright = getStringFromConfig("BookInfo", "copyright");
-
-	_numPages = getIntFromConfig("BookInfo", "nPages");
-	_numLanguages = getIntFromConfig("BookInfo", "nLanguages");
-	_screenWidth = getIntFromConfig("BookInfo", "xRes");
-	_screenHeight = getIntFromConfig("BookInfo", "yRes");
-	// nColors is here too, but it's always 256 anyway...
-
-	// this is 1 in The New Kid on the Block, changes the hardcoded UI
-	// v2 games changed the flag name to fPoetry
-	if (getGameType() == GType_LIVINGBOOKSV1)
-		_poetryMode = (getIntFromConfig("BookInfo", "poetry") == 1);
-	else
-		_poetryMode = (getIntFromConfig("BookInfo", "fPoetry") == 1);
-
-	// The later Living Books games add some more options:
-	//     - fNeedPalette                (always true?)
-	//     - fUse254ColorPalette         (always true?)
-	//     - nKBRequired                 (4096, RAM requirement?)
-	//     - fDebugWindow                (always 0?)
-
-	if (_bookInfoFile.hasSection("Globals")) {
-		const Common::INIFile::SectionKeyList globals = _bookInfoFile.getKeys("Globals");
-		for (Common::INIFile::SectionKeyList::const_iterator i = globals.begin(); i != globals.end(); i++) {
-			Common::String command = Common::String::format("%s = %s", i->key.c_str(), i->value.c_str());
-			LBCode tempCode(this, 0);
-			uint offset = tempCode.parseCode(command);
-			tempCode.runCode(nullptr, offset);
-		}
-	}
-}
-
-Common::String MohawkEngine_LivingBooks::stringForMode(LBMode mode) {
-	Common::String language = getStringFromConfig("Languages", Common::String::format("Language%d", _curLanguage));
-
-	switch (mode) {
-	case kLBIntroMode:
-		return "Intro";
-	case kLBControlMode:
-		return "Control";
-	case kLBCreditsMode:
-		return "Credits";
-	case kLBPreviewMode:
-		return "Preview";
-	case kLBReadMode:
-		return language + ".Read";
-	case kLBPlayMode:
-		return language + ".Play";
-	default:
-		error("unknown game mode %d", (int)mode);
-	}
-}
-
 void MohawkEngine_LivingBooks::destroyPage() {
 	_sound->stopSound();
 	_lastSoundOwner = 0;
@@ -798,169 +683,11 @@ void MohawkEngine_LivingBooks::lockSound(LBItem *owner, bool lock) {
 	}
 }
 
-// Only 1 VSRN resource per page
-uint16 LBPage::getResourceVersion() {
-	Common::SeekableReadStream *versionStream = _vm->getResource(ID_VRSN, _baseId);
-
-	// FIXME: some V2 games have very strange version entries
-	if (versionStream->size() != 2)
-		debug(1, "Version Record size mismatch");
-
-	uint16 version = versionStream->readUint16BE();
-
-	delete versionStream;
-	return version;
-}
-
-void LBPage::loadBITL(uint16 resourceId) {
-	Common::SeekableReadStreamEndian *bitlStream = _vm->wrapStreamEndian(ID_BITL, resourceId);
-
-	while (true) {
-		Common::Rect rect = _vm->readRect(bitlStream);
-		uint16 type = bitlStream->readUint16();
-
-		LBItem *res;
-		switch (type) {
-		case kLBPictureItem:
-			res = new LBPictureItem(_vm, this, rect);
-			break;
-		case kLBAnimationItem:
-			res = new LBAnimationItem(_vm, this, rect);
-			break;
-		case kLBPaletteItem:
-			res = new LBPaletteItem(_vm, this, rect);
-			break;
-		case kLBGroupItem:
-			res = new LBGroupItem(_vm, this, rect);
-			break;
-		case kLBSoundItem:
-			res = new LBSoundItem(_vm, this, rect);
-			break;
-		case kLBLiveTextItem:
-			res = new LBLiveTextItem(_vm, this, rect);
-			break;
-		case kLBMovieItem:
-			res = new LBMovieItem(_vm, this, rect);
-			break;
-		case kLBMiniGameItem:
-			res = new LBMiniGameItem(_vm, this, rect);
-			break;
-		case kLBProxyItem:
-			res = new LBProxyItem(_vm, this, rect);
-			break;
-		default:
-			warning("Unknown item type %04x", type);
-			// fall through
-		case 3: // often used for buttons
-			res = new LBItem(_vm, this, rect);
-			break;
-		}
-
-		res->readFrom(bitlStream);
-		_items.push_back(res);
-
-		if (bitlStream->size() == bitlStream->pos())
-			break;
-	}
-
-	delete bitlStream;
-}
-
 Common::SeekableReadStreamEndian *MohawkEngine_LivingBooks::wrapStreamEndian(uint32 tag, uint16 id) {
 	Common::SeekableReadStream *dataStream = getResource(tag, id);
 	return new Common::SeekableReadStreamEndianWrapper(dataStream, isBigEndian(), DisposeAfterUse::YES);
 }
 
-Common::String MohawkEngine_LivingBooks::getStringFromConfig(const Common::String &section, const Common::String &key) {
-	Common::String x, leftover;
-	_bookInfoFile.getKey(key, section, x);
-	Common::String tmp = removeQuotesFromString(x, leftover);
-	if (!leftover.empty())
-		warning("while parsing config key '%s' from section '%s', string '%s' was left after '%s'",
-			key.c_str(), section.c_str(), leftover.c_str(), tmp.c_str());
-	return tmp;
-}
-
-Common::String MohawkEngine_LivingBooks::getStringFromConfig(const Common::String &section, const Common::String &key, Common::String &leftover) {
-	Common::String x;
-	_bookInfoFile.getKey(key, section, x);
-	return removeQuotesFromString(x, leftover);
-}
-
-int MohawkEngine_LivingBooks::getIntFromConfig(const Common::String &section, const Common::String &key) {
-	return atoi(getStringFromConfig(section, key).c_str());
-}
-
-Common::String MohawkEngine_LivingBooks::getFileNameFromConfig(const Common::String &section, const Common::String &key, Common::String &leftover) {
-	Common::String string = getStringFromConfig(section, key, leftover);
-
-	if (string.hasPrefix("//")) {
-		// skip "//CD-ROM Title/" prefixes which we don't care about
-		uint i = 3;
-		while (i < string.size() && string[i - 1] != '/')
-			i++;
-
-		// Already uses slashes, no need to convert further
-		return string.c_str() + i;
-	}
-
-	return (getPlatform() == Common::kPlatformMacintosh) ? convertMacFileName(string) : convertWinFileName(string);
-}
-
-Common::String MohawkEngine_LivingBooks::removeQuotesFromString(const Common::String &string, Common::String &leftover) {
-	if (string.empty())
-		return string;
-
-	char quoteChar = string[0];
-	if (quoteChar != '\"' && quoteChar != '\'')
-		return string;
-
-	Common::String tmp;
-	bool inLeftover = false;
-	for (uint32 i = 1; i < string.size(); i++) {
-		if (inLeftover)
-			leftover += string[i];
-		else if (string[i] == quoteChar)
-			inLeftover = true;
-		else
-			tmp += string[i];
-	}
-
-	return tmp;
-}
-
-Common::String MohawkEngine_LivingBooks::convertMacFileName(const Common::String &string) {
-	Common::String filename;
-
-	for (uint32 i = 0; i < string.size(); i++) {
-		if (i == 0 && string[i] == ':') // First character should be ignored (another colon)
-			continue;
-		if (string[i] == ':') // Directory separator
-			filename += '/';
-		else if (string[i] == '/') // Literal slash
-			filename += ':'; // Replace by colon, as used by macOS for slash
-		else
-			filename += string[i];
-	}
-
-	return filename;
-}
-
-Common::String MohawkEngine_LivingBooks::convertWinFileName(const Common::String &string) {
-	Common::String filename;
-
-	for (uint32 i = 0; i < string.size(); i++) {
-		if (i == 0 && (string[i] == '/' || string[i] == '\\')) // ignore slashes at start
-			continue;
-		if (string[i] == '\\')
-			filename += '/';
-		else
-			filename += string[i];
-	}
-
-	return filename;
-}
-
 Archive *MohawkEngine_LivingBooks::createArchive() const {
 	if (isPreMohawk())
 		return new LivingBooksArchive_v1();
@@ -1032,285 +759,6 @@ void MohawkEngine_LivingBooks::nextPage() {
 	error("Could not find page after %d.%d for mode %d", _curPage, _curSubPage, (int)_curMode);
 }
 
-void MohawkEngine_LivingBooks::handleUIMenuClick(uint controlId) {
-	LBItem *item;
-
-	switch (controlId) {
-	case 1:
-		if (getFeatures() & GF_LB_10) {
-			if (!tryLoadPageStart(kLBControlMode, 2))
-				error("couldn't load options page");
-		} else {
-			if (!tryLoadPageStart(kLBControlMode, 3))
-				error("couldn't load options page");
-		}
-		break;
-
-	case 2:
-		item = getItemById(10);
-		if (item)
-			item->destroySelf();
-		item = getItemById(11);
-		if (item)
-			item->destroySelf();
-		item = getItemById(199 + _curLanguage);
-		if (item) {
-			item->setVisible(true);
-			item->togglePlaying(false, true);
-		}
-		break;
-
-	case 3:
-		item = getItemById(10);
-		if (item)
-			item->destroySelf();
-		item = getItemById(11);
-		if (item)
-			item->destroySelf();
-		item = getItemById(12);
-		if (item) {
-			item->setVisible(true);
-			item->togglePlaying(false, true);
-		}
-		break;
-
-	case 4:
-		if (getFeatures() & GF_LB_10) {
-			if (!tryLoadPageStart(kLBControlMode, 3))
-				error("couldn't load quit page");
-		} else {
-			if (!tryLoadPageStart(kLBControlMode, 2))
-				error("couldn't load quit page");
-		}
-		break;
-
-	case 10:
-		item = getItemById(10);
-		if (item)
-			item->destroySelf();
-		item = getItemById(11);
-		if (item) {
-			item->setVisible(true);
-			item->togglePlaying(false);
-		}
-		break;
-
-	case 11:
-		item = getItemById(11);
-		if (item)
-			item->togglePlaying(false, true);
-		break;
-
-	case 12:
-		// start game, in play mode
-		if (!tryLoadPageStart(kLBPlayMode, 1))
-			error("couldn't start play mode");
-		break;
-
-	default:
-		if (controlId >= 100 && controlId < 100 + (uint)_numLanguages) {
-			uint newLanguage = controlId - 99;
-			if (newLanguage == _curLanguage)
-				break;
-			item = getItemById(99 + _curLanguage);
-			if (item)
-				item->seek(1);
-			_curLanguage = newLanguage;
-		} else if (controlId >= 200 && controlId < 200 + (uint)_numLanguages) {
-			// start game, in read mode
-			if (!tryLoadPageStart(kLBReadMode, 1))
-				error("couldn't start read mode");
-		}
-		break;
-	}
-}
-
-void MohawkEngine_LivingBooks::handleUIPoetryMenuClick(uint controlId) {
-	LBItem *item;
-
-	// the menu UI in New Kid on the Block is a hybrid of the normal menu
-	// and the normal options screen
-
-	// TODO: this is mostly untested
-
-	switch (controlId) {
-	case 2:
-	case 3:
-		handleUIOptionsClick(controlId);
-		break;
-
-	case 4:
-		handleUIMenuClick(controlId);
-		break;
-
-	case 6:
-		handleUIMenuClick(2);
-		break;
-
-	case 7:
-		item = getItemById(10);
-		if (item)
-			item->destroySelf();
-		item = getItemById(11);
-		if (item)
-			item->destroySelf();
-		item = getItemById(12);
-		if (item) {
-			item->setVisible(true);
-			item->togglePlaying(false, true);
-		}
-		break;
-
-	case 0xA:
-		item = getItemById(10);
-		if (item)
-			item->destroySelf();
-		item = getItemById(11);
-		if (item) {
-			item->setVisible(true);
-			item->togglePlaying(false);
-		}
-		break;
-
-	case 0xB:
-		item = getItemById(11);
-		if (item)
-			item->togglePlaying(false, true);
-		break;
-
-	case 0xC:
-		if (!tryLoadPageStart(kLBPlayMode, _curSelectedPage))
-			error("failed to load page %d", _curSelectedPage);
-		break;
-
-	default:
-		if (controlId < 100) {
-			handleUIMenuClick(controlId);
-		} else {
-			if (!tryLoadPageStart(kLBReadMode, _curSelectedPage))
-				error("failed to load page %d", _curSelectedPage);
-		}
-	}
-}
-
-void MohawkEngine_LivingBooks::handleUIQuitClick(uint controlId) {
-	LBItem *item;
-
-	switch (controlId) {
-	case 1:
-	case 2:
-		// button clicked, run animation
-		item = getItemById(10);
-		if (item)
-			item->destroySelf();
-		item = getItemById(11);
-		if (item)
-			item->destroySelf();
-		item = getItemById((controlId == 1) ? 12 : 13);
-		if (item) {
-			item->setVisible(true);
-			item->togglePlaying(false);
-		}
-		break;
-
-	case 10:
-	case 11:
-		item = getItemById(11);
-		if (item)
-			item->togglePlaying(false, true);
-		break;
-
-	case 12:
-		// 'yes', I want to quit
-		quitGame();
-		break;
-
-	case 13:
-		// 'no', go back to menu
-		if (!tryLoadPageStart(kLBControlMode, 1))
-			error("couldn't return to menu");
-		break;
-
-	default:
-		break;
-	}
-}
-
-void MohawkEngine_LivingBooks::handleUIOptionsClick(uint controlId) {
-	LBItem *item;
-
-	switch (controlId) {
-	case 1:
-		item = getItemById(10);
-		if (item)
-			item->destroySelf();
-		item = getItemById(202);
-		if (item) {
-			item->setVisible(true);
-			item->togglePlaying(false, true);
-		}
-		break;
-
-	case 2:
-		// back
-		item = getItemById(2);
-		if (item)
-			item->seek(1);
-		if (_curSelectedPage == 1) {
-			_curSelectedPage = _numPages;
-		} else {
-			_curSelectedPage--;
-		}
-		for (uint i = 0; i < _numPages; i++) {
-			item = getItemById(1000 + i);
-			if (item)
-				item->setVisible(_curSelectedPage == i + 1);
-			item = getItemById(1100 + i);
-			if (item)
-				item->setVisible(_curSelectedPage == i + 1);
-		}
-		break;
-
-	case 3:
-		// forward
-		item = getItemById(3);
-		if (item)
-			item->seek(1);
-		if (_curSelectedPage == _numPages) {
-			_curSelectedPage = 1;
-		} else {
-			_curSelectedPage++;
-		}
-		for (uint i = 0; i < _numPages; i++) {
-			item = getItemById(1000 + i);
-			if (item)
-				item->setVisible(_curSelectedPage == i + 1);
-			item = getItemById(1100 + i);
-			if (item)
-				item->setVisible(_curSelectedPage == i + 1);
-		}
-		break;
-
-	case 4:
-		if (!tryLoadPageStart(kLBCreditsMode, 1))
-			error("failed to start credits");
-		break;
-
-	case 5:
-		if (!tryLoadPageStart(kLBPreviewMode, 1))
-			error("failed to start preview");
-		break;
-
-	case 202:
-		if (!tryLoadPageStart(kLBPlayMode, _curSelectedPage))
-			error("failed to load page %d", _curSelectedPage);
-		break;
-
-	default:
-		break;
-	}
-}
-
 void MohawkEngine_LivingBooks::handleNotify(NotifyEvent &event) {
 	// hard-coded behavior (GUI/navigation)
 
@@ -1469,610 +917,49 @@ void MohawkEngine_LivingBooks::handleNotify(NotifyEvent &event) {
 	}
 }
 
-LBAnimationNode::LBAnimationNode(MohawkEngine_LivingBooks *vm, LBAnimation *parent, uint16 scriptResourceId) : _vm(vm), _parent(parent) {
-	loadScript(scriptResourceId);
-}
-
-LBAnimationNode::~LBAnimationNode() {
-	for (uint32 i = 0; i < _scriptEntries.size(); i++)
-		delete[] _scriptEntries[i].data;
+LBScriptEntry::LBScriptEntry() {
+	state = 0;
+	data = nullptr;
+	argvParam = nullptr;
+	argvTarget = nullptr;
 }
 
-void LBAnimationNode::loadScript(uint16 resourceId) {
-	Common::SeekableReadStreamEndian *scriptStream = _vm->wrapStreamEndian(ID_SCRP, resourceId);
-
-	reset();
-
-	while (byte opcodeId = scriptStream->readByte()) {
-		byte size = scriptStream->readByte();
-
-		LBAnimScriptEntry entry;
-		entry.opcode = opcodeId;
-		entry.size = size;
-
-		if (!size) {
-			entry.data = nullptr;
-		} else {
-			entry.data = new byte[entry.size];
-			scriptStream->read(entry.data, entry.size);
-		}
-
-		_scriptEntries.push_back(entry);
-	}
-
-	byte size = scriptStream->readByte();
-	if (size != 0 || scriptStream->pos() != scriptStream->size())
-		error("Failed to read script correctly");
+LBScriptEntry::~LBScriptEntry() {
+	delete[] argvParam;
+	delete[] argvTarget;
+	delete[] data;
 
-	delete scriptStream;
+	for (uint i = 0; i < subentries.size(); i++)
+		delete subentries[i];
 }
 
-void LBAnimationNode::draw(const Common::Rect &_bounds) {
-	if (!_currentCel)
-		return;
-
-	// this is also checked in SetCel, below
-	if (_currentCel > _parent->getNumResources())
-		error("Animation cel %d was too high, this shouldn't happen!", _currentCel);
-
-	int16 xOffset = _xPos + _bounds.left;
-	int16 yOffset = _yPos + _bounds.top;
+LBItem::LBItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : _vm(vm), _page(page), _rect(rect) {
+	if (_vm->getGameType() == GType_LIVINGBOOKSV1 || _vm->getGameType() == GType_LIVINGBOOKSV2)
+		_phase = kLBPhaseInit;
+	else
+		_phase = kLBPhaseLoad;
 
-	uint16 resourceId = _parent->getResource(_currentCel - 1);
+	_loopMode = 0;
+	_delayMin = 0;
+	_delayMax = 0;
+	_timingMode = kLBAutoNone;
+	_periodMin = 0;
+	_periodMax = 0;
+	_controlMode = kLBControlNone;
+	_soundMode = 0;
 
-	if (!_vm->isPreMohawk()) {
-		Common::Point offset = _parent->getOffset(_currentCel - 1);
-		xOffset -= offset.x;
-		yOffset -= offset.y;
-	}
+	_loaded = false;
+	_enabled = false;
+	_visible = true;
+	_playing = false;
+	_globalEnabled = true;
+	_globalVisible = true;
+	_nextTime = 0;
+	_startTime = 0;
+	_loops = 0;
 
-	_vm->_gfx->copyOffsetAnimImageToScreen(resourceId, xOffset, yOffset);
-}
-
-void LBAnimationNode::reset() {
-	// TODO: this causes stupid flickering
-	//if (_currentCel)
-	//	_vm->_needsRedraw = true;
-
-	_currentCel = 0;
-	_currentEntry = 0;
-	_delay = 0;
-
-	_xPos = 0;
-	_yPos = 0;
-}
-
-NodeState LBAnimationNode::update(bool seeking) {
-	if (_currentEntry == _scriptEntries.size())
-		return kLBNodeDone;
-
-	if (_delay > 0 && --_delay)
-		return kLBNodeRunning;
-
-	while (_currentEntry < _scriptEntries.size()) {
-		LBAnimScriptEntry &entry = _scriptEntries[_currentEntry];
-		_currentEntry++;
-		debug(5, "Running script entry %d of %d", _currentEntry, _scriptEntries.size());
-
-		switch (entry.opcode) {
-		case kLBAnimOpPlaySound:
-		case kLBAnimOpWaitForSound:
-		case kLBAnimOpReleaseSound:
-		case kLBAnimOpResetSound:
-			{
-			uint16 soundResourceId = READ_BE_UINT16(entry.data);
-
-			if (!soundResourceId) {
-				error("Unhandled named wave file, tell clone2727 where you found this");
-				break;
-			}
-
-			Common::String cue;
-			uint pos = 2;
-			while (pos < entry.size) {
-				char in = entry.data[pos];
-				if (!in)
-					break;
-				pos++;
-				cue += in;
-			}
-			if (pos == entry.size)
-				error("Cue in sound kLBAnimOp wasn't null-terminated");
-
-			switch (entry.opcode) {
-			case kLBAnimOpPlaySound:
-				if (seeking)
-					break;
-				debug(4, "a: PlaySound(%0d)", soundResourceId);
-				_parent->playSound(soundResourceId);
-				break;
-			case kLBAnimOpWaitForSound:
-				if (seeking)
-					break;
-				debug(4, "b: WaitForSound(%0d)", soundResourceId);
-				if (!_parent->soundPlaying(soundResourceId, cue))
-					break;
-				_currentEntry--;
-				return kLBNodeWaiting;
-			case kLBAnimOpReleaseSound:
-				debug(4, "c: ReleaseSound(%0d)", soundResourceId);
-				// TODO
-				_vm->_sound->stopSound(soundResourceId);
-				break;
-			case kLBAnimOpResetSound:
-				debug(4, "d: ResetSound(%0d)", soundResourceId);
-				// TODO
-				_vm->_sound->stopSound(soundResourceId);
-				break;
-			default:
-				break;
-			}
-			}
-			break;
-
-		case kLBAnimOpSetTempo:
-		case kLBAnimOpSetTempoDiv:
-			{
-			assert(entry.size == 2);
-			uint16 tempo = (int16)READ_BE_UINT16(entry.data);
-
-			// TODO: LB 3 uses fixed-point here.
-			if (entry.opcode == kLBAnimOpSetTempo) {
-				debug(4, "3: SetTempo(%d)", tempo);
-				// TODO: LB 3 uses (tempo * 1000) / 60, while
-				// the original divides the system time by 16.
-				_parent->setTempo(tempo * 16);
-			} else {
-				// LB 3.0+ only.
-				debug(4, "E: SetTempoDiv(%d)", tempo);
-				_parent->setTempo(1000 / tempo);
-			}
-
-			}
-			break;
-
-		case kLBAnimOpWait:
-			assert(entry.size == 0);
-			debug(5, "6: Wait()");
-			return kLBNodeRunning;
-
-		case kLBAnimOpMoveTo:
-			{
-			assert(entry.size == 4);
-			int16 x = (int16)READ_BE_UINT16(entry.data);
-			int16 y = (int16)READ_BE_UINT16(entry.data + 2);
-			debug(4, "5: MoveTo(%d, %d)", x, y);
-
-			_xPos = x;
-			_yPos = y;
-			_vm->_needsRedraw = true;
-			}
-			break;
-
-		case kLBAnimOpDrawMode:
-			{
-			assert(entry.size == 2);
-			uint16 mode = (int16)READ_BE_UINT16(entry.data);
-			debug(4, "9: DrawMode(%d)", mode);
-
-			// TODO
-			}
-			break;
-
-		case kLBAnimOpSetCel:
-			{
-			assert(entry.size == 2);
-			uint16 cel = (int16)READ_BE_UINT16(entry.data);
-			debug(4, "7: SetCel(%d)", cel);
-
-			_currentCel = cel;
-			if (_currentCel > _parent->getNumResources())
-				error("SetCel set current cel to %d, but we only have %d cels", _currentCel, _parent->getNumResources());
-			_vm->_needsRedraw = true;
-			}
-			break;
-
-		case kLBAnimOpNotify:
-			{
-			assert(entry.size == 2);
-			uint16 data = (int16)READ_BE_UINT16(entry.data);
-
-			if (seeking)
-				break;
-
-			debug(4, "2: Notify(%d)", data);
-			_vm->notifyAll(data, _parent->getParentId());
-			}
-			break;
-
-		case kLBAnimOpSleepUntil:
-			{
-			assert(entry.size == 4);
-			uint32 frame = READ_BE_UINT32(entry.data);
-			debug(4, "8: SleepUntil(%d)", frame);
-
-			if (frame > _parent->getCurrentFrame()) {
-				// *not* kLBNodeWaiting
-				_currentEntry--;
-				return kLBNodeRunning;
-			}
-			}
-			break;
-
-		case kLBAnimOpDelay:
-			{
-			assert(entry.size == 4);
-			uint32 delay = READ_BE_UINT32(entry.data);
-			debug(4, "f: Delay(%d)", delay);
-			_delay = delay;
-			return kLBNodeRunning;
-			}
-			break;
-
-		default:
-			error("Unknown opcode id %02x (size %d)", entry.opcode, entry.size);
-			break;
-		}
-	}
-
-	return kLBNodeRunning;
-}
-
-bool LBAnimationNode::transparentAt(int x, int y) {
-	if (!_currentCel)
-		return true;
-
-	uint16 resourceId = _parent->getResource(_currentCel - 1);
-
-	if (!_vm->isPreMohawk()) {
-		Common::Point offset = _parent->getOffset(_currentCel - 1);
-		x += offset.x;
-		y += offset.y;
-	}
-
-	// TODO: only check pixels if necessary
-	return _vm->_gfx->imageIsTransparentAt(resourceId, true, x - _xPos, y - _yPos);
-}
-
-LBAnimation::LBAnimation(MohawkEngine_LivingBooks *vm, LBAnimationItem *parent, uint16 resourceId) : _vm(vm), _parent(parent) {
-	Common::SeekableReadStreamEndian *aniStream = _vm->wrapStreamEndian(ID_ANI, resourceId);
-
-	// ANI records in the Wanderful sampler are 32 bytes, extra bytes are just NULs
-	if (aniStream->size() != 30 && aniStream->size() != 32)
-		warning("ANI Record size mismatch");
-
-	uint16 version = aniStream->readUint16();
-	if (version != 1)
-		warning("ANI version not 1");
-
-	_bounds = _vm->readRect(aniStream);
-	_clip = _vm->readRect(aniStream);
-	// TODO: what is colorId for?
-	uint32 colorId = aniStream->readUint32();
-	uint32 sprResourceId = aniStream->readUint32();
-	uint32 sprResourceOffset = aniStream->readUint32();
-
-	debug(5, "ANI bounds: (%d, %d), (%d, %d)", _bounds.left, _bounds.top, _bounds.right, _bounds.bottom);
-	debug(5, "ANI clip: (%d, %d), (%d, %d)", _clip.left, _clip.top, _clip.right, _clip.bottom);
-	debug(5, "ANI color id: %d", colorId);
-	debug(5, "ANI SPRResourceId: %d, offset %d", sprResourceId, sprResourceOffset);
-	if (aniStream->size() == 32) {
-		debug(5, "ANI extra bytes: (%d)", aniStream->readUint16());
-	}
-
-	if (aniStream->pos() != aniStream->size())
-		error("Still %d bytes at the end of anim stream", (int)(aniStream->size() - aniStream->pos()));
-
-	delete aniStream;
-
-	if (sprResourceOffset)
-		error("Cannot handle non-zero ANI offset yet");
-
-	Common::SeekableReadStreamEndian *sprStream = _vm->wrapStreamEndian(ID_SPR, sprResourceId);
-
-	uint16 numBackNodes = sprStream->readUint16();
-	uint16 numFrontNodes = sprStream->readUint16();
-	uint32 shapeResourceID = sprStream->readUint32();
-	uint32 shapeResourceOffset = sprStream->readUint32();
-	uint32 scriptResourceID = sprStream->readUint32();
-	uint32 scriptResourceOffset = sprStream->readUint32();
-	uint32 scriptResourceLength = sprStream->readUint32();
-	debug(5, "SPR# stream: %d front, %d background", numFrontNodes, numBackNodes);
-	debug(5, "Shape ID %d (offset 0x%04x), script ID %d (offset 0x%04x, length %d)", shapeResourceID, shapeResourceOffset,
-		scriptResourceID, scriptResourceOffset, scriptResourceLength);
-
-	Common::Array<uint16> scriptIDs;
-	for (uint16 i = 0; i < numFrontNodes; i++) {
-		uint32 unknown1 = sprStream->readUint32();
-		uint32 unknown2 = sprStream->readUint32();
-		uint32 unknown3 = sprStream->readUint32();
-		uint16 scriptID = sprStream->readUint32();
-		uint32 unknown4 = sprStream->readUint32();
-		uint32 unknown5 = sprStream->readUint32();
-		scriptIDs.push_back(scriptID);
-		debug(6, "Front node %d: script ID %d", i, scriptID);
-		if (unknown1 != 0 || unknown2 != 0 || unknown3 != 0 || unknown4 != 0 || unknown5 != 0)
-			error("Anim node %d had non-zero unknowns %08x, %08x, %08x, %08x, %08x",
-				i, unknown1, unknown2, unknown3, unknown4, unknown5);
-	}
-
-	if (numBackNodes)
-		error("Ignoring %d back nodes", numBackNodes);
-
-	if (sprStream->pos() != sprStream->size())
-		error("Still %d bytes at the end of sprite stream", (int)(sprStream->size() - sprStream->pos()));
-
-	delete sprStream;
-
-	loadShape(shapeResourceID);
-
-	_nodes.push_back(new LBAnimationNode(_vm, this, scriptResourceID));
-	for (uint16 i = 0; i < scriptIDs.size(); i++)
-		_nodes.push_back(new LBAnimationNode(_vm, this, scriptIDs[i]));
-
-	_currentFrame = 0;
-	_currentSound = 0xffff;
-	_running = false;
-	_tempo = 1;
-}
-
-LBAnimation::~LBAnimation() {
-	for (uint32 i = 0; i < _nodes.size(); i++)
-		delete _nodes[i];
-	if (_currentSound != 0xffff)
-		_vm->_sound->stopSound(_currentSound);
-}
-
-void LBAnimation::loadShape(uint16 resourceId) {
-	if (resourceId == 0)
-		return;
-
-	Common::SeekableReadStreamEndian *shapeStream = _vm->wrapStreamEndian(ID_SHP, resourceId);
-
-	if (_vm->isPreMohawk()) {
-		if (shapeStream->size() < 6)
-			error("V1 SHP Record size too short (%d)", (int)shapeStream->size());
-
-		uint16 u0 = shapeStream->readUint16();
-		if (u0 != 3)
-			error("V1 SHP Record u0 is %04x, not 3", u0);
-
-		uint16 u1 = shapeStream->readUint16();
-		if (u1 != 0)
-			error("V1 SHP Record u1 is %04x, not 0", u1);
-
-		uint16 idCount = shapeStream->readUint16();
-		debug(8, "V1 SHP: idCount: %d", idCount);
-
-		if (shapeStream->size() != (idCount * 2) + 6)
-			error("V1 SHP Record size mismatch (%d)", (int)shapeStream->size());
-
-		for (uint16 i = 0; i < idCount; i++) {
-			_shapeResources.push_back(shapeStream->readUint16());
-			debug(8, "V1 SHP: BMAP Resource Id %d: %d", i, _shapeResources[i]);
-		}
-	} else {
-		uint16 idCount = shapeStream->readUint16();
-		debug(8, "SHP: idCount: %d", idCount);
-
-		if (shapeStream->size() != (idCount * 6) + 2)
-			error("SHP Record size mismatch (%d)", (int)shapeStream->size());
-
-		for (uint16 i = 0; i < idCount; i++) {
-			_shapeResources.push_back(shapeStream->readUint16());
-			int16 x = shapeStream->readSint16();
-			int16 y = shapeStream->readSint16();
-			_shapeOffsets.push_back(Common::Point(x, y));
-			debug(8, "SHP: tBMP Resource Id %d: %d, at (%d, %d)", i, _shapeResources[i], x, y);
-		}
-	}
-
-	for (uint16 i = 0; i < _shapeResources.size(); i++)
-		_vm->_gfx->preloadImage(_shapeResources[i]);
-
-	delete shapeStream;
-}
-
-void LBAnimation::draw() {
-	for (uint32 i = 0; i < _nodes.size(); i++)
-		_nodes[i]->draw(_bounds);
-}
-
-bool LBAnimation::update() {
-	if (!_running)
-		return false;
-
-	if (_vm->_system->getMillis() <= _lastTime + (uint32)_tempo)
-		return false;
-
-	// the second check is to try 'catching up' with lagged animations, might be crazy
-	if (_lastTime == 0 || (_vm->_system->getMillis()) > _lastTime + (uint32)(_tempo * 2))
-		_lastTime = _vm->_system->getMillis();
-	else
-		_lastTime += _tempo;
-
-	if (_currentSound != 0xffff && !_vm->_sound->isPlaying(_currentSound)) {
-		_currentSound = 0xffff;
-	}
-
-	NodeState state = kLBNodeDone;
-	for (uint32 i = 0; i < _nodes.size(); i++) {
-		NodeState s = _nodes[i]->update();
-		if (s == kLBNodeWaiting) {
-			state = kLBNodeWaiting;
-			if (i != 0)
-				warning("non-primary node was waiting");
-			break;
-		}
-		if (s == kLBNodeRunning)
-			state = kLBNodeRunning;
-	}
-
-	if (state == kLBNodeRunning) {
-		_currentFrame++;
-	} else if (state == kLBNodeDone) {
-		if (_currentSound == 0xffff) {
-			_running = false;
-			return true;
-		}
-	}
-
-	return false;
-}
-
-void LBAnimation::start() {
-	_lastTime = 0;
-	_running = true;
-}
-
-void LBAnimation::seek(uint16 pos) {
-	_lastTime = 0;
-	_currentFrame = 0;
-
-	if (_currentSound != 0xffff) {
-		_vm->_sound->stopSound(_currentSound);
-		_currentSound = 0xffff;
-	}
-
-	for (uint32 i = 0; i < _nodes.size(); i++)
-		_nodes[i]->reset();
-
-	for (uint16 n = 0; n < pos; n++) {
-		bool ranSomething = false;
-		// nodes don't wait while seeking
-		for (uint32 i = 0; i < _nodes.size(); i++)
-			ranSomething |= (_nodes[i]->update(true) != kLBNodeDone);
-
-		_currentFrame++;
-
-		if (!ranSomething) {
-			_running = false;
-			break;
-		}
-	}
-}
-
-void LBAnimation::seekToTime(uint32 time) {
-	_lastTime = 0;
-	_currentFrame = 0;
-
-	if (_currentSound != 0xffff) {
-		_vm->_sound->stopSound(_currentSound);
-		_currentSound = 0xffff;
-	}
-
-	for (uint32 i = 0; i < _nodes.size(); i++)
-		_nodes[i]->reset();
-
-	uint32 elapsed = 0;
-	while (elapsed <= time) {
-		bool ranSomething = false;
-		// nodes don't wait while seeking
-		for (uint32 i = 0; i < _nodes.size(); i++)
-			ranSomething |= (_nodes[i]->update(true) != kLBNodeDone);
-
-		elapsed += _tempo;
-		_currentFrame++;
-
-		if (!ranSomething) {
-			_running = false;
-			break;
-		}
-	}
-}
-
-void LBAnimation::stop() {
-	_running = false;
-	if (_currentSound != 0xffff) {
-		_vm->_sound->stopSound(_currentSound);
-		_currentSound = 0xffff;
-	}
-}
-
-void LBAnimation::playSound(uint16 resourceId) {
-	_currentSound = resourceId;
-	_vm->_sound->playSound(_currentSound, Audio::Mixer::kMaxChannelVolume, false, &_cueList);
-}
-
-bool LBAnimation::soundPlaying(uint16 resourceId, const Common::String &cue) {
-	if (_currentSound != resourceId)
-		return false;
-	if (!_vm->_sound->isPlaying(_currentSound))
-		return false;
-
-	if (cue.empty())
-		return true;
-
-	uint samples = _vm->_sound->getNumSamplesPlayed(_currentSound);
-	for (uint i = 0; i < _cueList.pointCount; i++) {
-		if (_cueList.points[i].sampleFrame > samples)
-			break;
-		if (_cueList.points[i].name == cue)
-			return false;
-	}
-
-	return true;
-}
-
-bool LBAnimation::transparentAt(int x, int y) {
-	for (uint32 i = 0; i < _nodes.size(); i++)
-		if (!_nodes[i]->transparentAt(x - _bounds.left, y - _bounds.top))
-			return false;
-
-	return true;
-}
-
-void LBAnimation::setTempo(uint16 tempo) {
-	_tempo = tempo;
-}
-
-uint16 LBAnimation::getParentId() {
-	return _parent->getId();
-}
-
-LBScriptEntry::LBScriptEntry() {
-	state = 0;
-	data = nullptr;
-	argvParam = nullptr;
-	argvTarget = nullptr;
-}
-
-LBScriptEntry::~LBScriptEntry() {
-	delete[] argvParam;
-	delete[] argvTarget;
-	delete[] data;
-
-	for (uint i = 0; i < subentries.size(); i++)
-		delete subentries[i];
-}
-
-LBItem::LBItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : _vm(vm), _page(page), _rect(rect) {
-	if (_vm->getGameType() == GType_LIVINGBOOKSV1 || _vm->getGameType() == GType_LIVINGBOOKSV2)
-		_phase = kLBPhaseInit;
-	else
-		_phase = kLBPhaseLoad;
-
-	_loopMode = 0;
-	_delayMin = 0;
-	_delayMax = 0;
-	_timingMode = kLBAutoNone;
-	_periodMin = 0;
-	_periodMax = 0;
-	_controlMode = kLBControlNone;
-	_soundMode = 0;
-
-	_loaded = false;
-	_enabled = false;
-	_visible = true;
-	_playing = false;
-	_globalEnabled = true;
-	_globalVisible = true;
-	_nextTime = 0;
-	_startTime = 0;
-	_loops = 0;
-
-	_isAmbient = false;
-	_doHitTest = true;
+	_isAmbient = false;
+	_doHitTest = true;
 }
 
 LBItem::~LBItem() {
@@ -3146,857 +2033,4 @@ bool LBItem::checkCondition(const Common::String &condition) {
 	return result.toInt();
 }
 
-LBSoundItem::LBSoundItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
-	debug(3, "new LBSoundItem");
-	_running = false;
-}
-
-LBSoundItem::~LBSoundItem() {
-	if (_running)
-		_vm->_sound->stopSound(_resourceId);
-}
-
-void LBSoundItem::update() {
-	if (_running && !_vm->_sound->isPlaying(_resourceId)) {
-		_running = false;
-		done(true);
-	}
-
-	LBItem::update();
-}
-
-bool LBSoundItem::togglePlaying(bool playing, bool restart) {
-	if (!playing)
-		return LBItem::togglePlaying(playing, restart);
-
-	if (_running) {
-		_running = false;
-		_vm->_sound->stopSound(_resourceId);
-	}
-
-	if (!_loaded || !_enabled || !_globalEnabled)
-		return false;
-
-	_running = true;
-	debug(4, "sound %d play for item %d (%s)", _resourceId, _itemId, _desc.c_str());
-	_vm->playSound(this, _resourceId);
-	return true;
-}
-
-void LBSoundItem::stop() {
-	if (_running) {
-		_running = false;
-		_vm->_sound->stopSound(_resourceId);
-	}
-
-	LBItem::stop();
-}
-
-LBItem *LBSoundItem::createClone() {
-	return new LBSoundItem(_vm, _page, _rect);
-}
-
-LBGroupItem::LBGroupItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
-	debug(3, "new LBGroupItem");
-	_starting = false;
-}
-
-void LBGroupItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
-	switch (type) {
-	case kLBGroupData:
-		{
-		_groupEntries.clear();
-		uint16 count = stream->readUint16();
-		debug(3, "Group data: %d entries", count);
-
-		if (size != 2 + count * 4)
-			error("kLBGroupData was wrong size (%d, for %d entries)", size, count);
-
-		for (uint i = 0; i < count; i++) {
-			GroupEntry entry;
-			// TODO: is type important for any game? at the moment, we ignore it
-			entry.entryType = stream->readUint16();
-			entry.entryId = stream->readUint16();
-			// HACK: The Living Books v3 sampler includes the ID for the group as
-			// one of the entries in the Green Eggs and Ham section, which leads
-			// to infinite recursion when the group is loaded.
-			if (entry.entryId != getId())
-				_groupEntries.push_back(entry);
-			debug(3, "group entry: id %d, type %d", entry.entryId, entry.entryType);
-		}
-		}
-		break;
-
-	default:
-		LBItem::readData(type, size, stream);
-	}
-}
-
-void LBGroupItem::destroySelf() {
-	LBItem::destroySelf();
-
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->destroySelf();
-	}
-}
-
-void LBGroupItem::setEnabled(bool enabled) {
-	if (_starting) {
-		_starting = false;
-		LBItem::setEnabled(enabled);
-	} else {
-		for (uint i = 0; i < _groupEntries.size(); i++) {
-			LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-			if (item)
-				item->setEnabled(enabled);
-		}
-	}
-}
-
-void LBGroupItem::setGlobalEnabled(bool enabled) {
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->setGlobalEnabled(enabled);
-	}
-}
-
-bool LBGroupItem::contains(Common::Point point) {
-	return false;
-}
-
-bool LBGroupItem::togglePlaying(bool playing, bool restart) {
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->togglePlaying(playing, restart);
-	}
-
-	return false;
-}
-
-void LBGroupItem::seek(uint16 pos) {
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->seek(pos);
-	}
-}
-
-void LBGroupItem::setVisible(bool visible) {
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->setVisible(visible);
-	}
-}
-
-void LBGroupItem::setGlobalVisible(bool visible) {
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->setGlobalVisible(visible);
-	}
-}
-
-void LBGroupItem::startPhase(uint phase) {
-	_starting = true;
-	LBItem::startPhase(phase);
-	_starting = false;
-}
-
-void LBGroupItem::stop() {
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->stop();
-	}
-}
-
-void LBGroupItem::load() {
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->load();
-	}
-}
-
-void LBGroupItem::unload() {
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->unload();
-	}
-}
-
-void LBGroupItem::moveBy(const Common::Point &pos) {
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->moveBy(pos);
-	}
-}
-
-void LBGroupItem::moveTo(const Common::Point &pos) {
-	for (uint i = 0; i < _groupEntries.size(); i++) {
-		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
-		if (item)
-			item->moveTo(pos);
-	}
-}
-
-LBItem *LBGroupItem::createClone() {
-	// TODO: needed?
-	error("LBGroupItem::createClone unimplemented");
-	return new LBGroupItem(_vm, _page, _rect);
-}
-
-LBPaletteItem::LBPaletteItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
-	debug(3, "new LBPaletteItem");
-
-	_fadeInStart = 0;
-	_palette = nullptr;
-}
-
-LBPaletteItem::~LBPaletteItem() {
-	delete[] _palette;
-}
-
-void LBPaletteItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
-	switch (type) {
-	case kLBPaletteXData:
-		{
-		assert(size >= 8);
-		_fadeInPeriod = stream->readUint16();
-		_fadeInStep = stream->readUint16();
-		_drawStart = stream->readUint16();
-		_drawCount = stream->readUint16();
-		if (_drawStart + _drawCount > 256)
-			error("encountered palette trying to set more than 256 colors");
-		assert(size == 8 + _drawCount * 4);
-
-		// TODO: _drawCount is really more like _drawEnd, so once we're sure that
-		// there's really no use for the palette entries before _drawCount, we
-		// might want to just discard them here, at load time.
-		_palette = new byte[_drawCount * 3];
-		for (uint i = 0; i < _drawCount; i++) {
-			_palette[i*3 + 0] = stream->readByte();
-			_palette[i*3 + 1] = stream->readByte();
-			_palette[i*3 + 2] = stream->readByte();
-			stream->readByte();
-		}
-		}
-		break;
-
-	default:
-		LBItem::readData(type, size, stream);
-	}
-}
-
-bool LBPaletteItem::togglePlaying(bool playing, bool restart) {
-	// TODO: this likely isn't the right place
-
-	if (playing) {
-		_fadeInStart = _vm->_system->getMillis();
-		_fadeInCurrent = 0;
-
-		return true;
-	}
-
-	return LBItem::togglePlaying(playing, restart);
-}
-
-void LBPaletteItem::update() {
-	if (_fadeInStart) {
-		if (!_palette)
-			error("LBPaletteItem had no palette on startup");
-
-		uint32 elapsedTime = _vm->_system->getMillis() - _fadeInStart;
-		uint32 divTime = elapsedTime / _fadeInStep;
-
-		if (divTime > _fadeInPeriod)
-			divTime = _fadeInPeriod;
-
-		if (_fadeInCurrent != divTime) {
-			_fadeInCurrent = divTime;
-
-			// TODO: actual fading-in
-			if (_visible && _globalVisible) {
-				_vm->_system->getPaletteManager()->setPalette(_palette + _drawStart * 3, _drawStart, _drawCount - _drawStart);
-				_vm->_needsRedraw = true;
-			}
-		}
-
-		if (elapsedTime >= (uint32)_fadeInPeriod * (uint32)_fadeInStep) {
-			// TODO: correct?
-			_fadeInStart = 0;
-		}
-	}
-
-	LBItem::update();
-}
-
-LBItem *LBPaletteItem::createClone() {
-	error("can't clone LBPaletteItem");
-}
-
-LBLiveTextItem::LBLiveTextItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
-	_currentPhrase = 0xFFFF;
-	_currentWord = 0xFFFF;
-	debug(3, "new LBLiveTextItem");
-}
-
-void LBLiveTextItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
-	switch (type) {
-	case kLBLiveTextData:
-		{
-		stream->read(_backgroundColor, 4); // unused?
-		stream->read(_foregroundColor, 4);
-		stream->read(_highlightColor, 4);
-		_paletteIndex = stream->readUint16();
-		uint16 phraseCount = stream->readUint16();
-		uint16 wordCount = stream->readUint16();
-
-		debug(3, "LiveText has %d words in %d phrases, palette index 0x%04x", wordCount, phraseCount, _paletteIndex);
-		debug(3, "LiveText colors: background %02x%02x%02x%02x, foreground %02x%02x%02x%02x, highlight %02x%02x%02x%02x",
-			_backgroundColor[0], _backgroundColor[1], _backgroundColor[2], _backgroundColor[3],
-			_foregroundColor[0], _foregroundColor[1], _foregroundColor[2], _foregroundColor[3],
-			_highlightColor[0], _highlightColor[1], _highlightColor[2], _highlightColor[3]);
-
-		if (size != 18 + 14 * wordCount + 18 * phraseCount)
-			error("Bad Live Text data size (got %d, wanted %d words and %d phrases)", size, wordCount, phraseCount);
-
-		_words.clear();
-		for (uint i = 0; i < wordCount; i++) {
-			LiveTextWord word;
-			word.bounds = _vm->readRect(stream);
-			word.soundId = stream->readUint16();
-			word.itemType = stream->readUint16();
-			word.itemId = stream->readUint16();
-			debug(4, "Word: (%d, %d) to (%d, %d), sound %d, item %d (type %d)",
-				word.bounds.left, word.bounds.top, word.bounds.right, word.bounds.bottom, word.soundId, word.itemId, word.itemType);
-			_words.push_back(word);
-		}
-
-		_phrases.clear();
-		for (uint i = 0; i < phraseCount; i++) {
-			LiveTextPhrase phrase;
-			phrase.wordStart = stream->readUint16();
-			phrase.wordCount = stream->readUint16();
-			phrase.highlightStart = stream->readUint16();
-			phrase.startId = stream->readUint16();
-			phrase.highlightEnd = stream->readUint16();
-			phrase.endId = stream->readUint16();
-
-			// The original stored the values in uint32's so we need to swap here
-			if (_vm->isBigEndian()) {
-				SWAP(phrase.highlightStart, phrase.startId);
-				SWAP(phrase.highlightEnd, phrase.endId);
-			}
-
-			uint32 unknown1 = stream->readUint16();
-			uint16 unknown2 = stream->readUint32();
-
-			if (unknown1 != 0 || unknown2 != 0)
-				error("Unexpected unknowns %08x/%04x in LiveText word", unknown1, unknown2);
-
-			debug(4, "Phrase: start %d, count %d, start at %d (from %d), end at %d (from %d)",
-				phrase.wordStart, phrase.wordCount, phrase.highlightStart, phrase.startId, phrase.highlightEnd, phrase.endId);
-
-			_phrases.push_back(phrase);
-		}
-		}
-		break;
-
-	default:
-		LBItem::readData(type, size, stream);
-	}
-}
-
-bool LBLiveTextItem::contains(Common::Point point) {
-	if (!LBItem::contains(point))
-		return false;
-
-	point.x -= _rect.left;
-	point.y -= _rect.top;
-
-	for (uint i = 0; i < _words.size(); i++) {
-		if (_words[i].bounds.contains(point))
-			return true;
-	}
-
-	return false;
-}
-
-void LBLiveTextItem::paletteUpdate(uint16 word, bool on) {
-	_vm->_needsRedraw = true;
-
-	// Sometimes the last phrase goes out-of-bounds, the original engine
-	// only checks the words which are valid in the palette updating code.
-	if (word >= _words.size())
-		return;
-
-	if (_resourceId) {
-		// with a resource, we draw a bitmap in draw() rather than changing the palette
-		return;
-	}
-
-	if (on) {
-		_vm->_system->getPaletteManager()->setPalette(_highlightColor, _paletteIndex + word, 1);
-	} else {
-		_vm->_system->getPaletteManager()->setPalette(_foregroundColor, _paletteIndex + word, 1);
-	}
-}
-
-void LBLiveTextItem::update() {
-	if (_currentWord != 0xFFFF) {
-		uint16 soundId = _words[_currentWord].soundId;
-		if (soundId && !_vm->_sound->isPlaying(soundId)) {
-			paletteUpdate(_currentWord, false);
-
-			// TODO: check this in RE
-			LBItem *item = _vm->getItemById(_words[_currentWord].itemId);
-			if (item)
-				item->togglePlaying(false, true);
-
-			_currentWord = 0xFFFF;
-		}
-	}
-
-	LBItem::update();
-}
-
-void LBLiveTextItem::draw() {
-	// this is only necessary when we are drawing using a bitmap
-	if (!_resourceId)
-		return;
-
-	if (_currentWord != 0xFFFF) {
-		uint yPos = 0;
-		if (_currentWord > 0) {
-			for (uint i = 0; i < _currentWord; i++) {
-				yPos += (_words[i].bounds.bottom - _words[i].bounds.top);
-			}
-		}
-		drawWord(_currentWord, yPos);
-		return;
-	}
-
-	if (_currentPhrase == 0xFFFF)
-		return;
-
-	uint wordStart = _phrases[_currentPhrase].wordStart;
-	uint wordCount = _phrases[_currentPhrase].wordCount;
-	if (wordStart + wordCount > _words.size())
-		error("phrase %d was invalid (%d words, from %d, out of only %d total)",
-			_currentPhrase, wordCount, wordStart, _words.size());
-
-	uint yPos = 0;
-	for (uint i = 0; i < wordStart + wordCount; i++) {
-		if (i >= wordStart)
-			drawWord(i, yPos);
-		yPos += (_words[i].bounds.bottom - _words[i].bounds.top);
-	}
-}
-
-void LBLiveTextItem::drawWord(uint word, uint yPos) {
-	Common::Rect srcRect(0, yPos, _words[word].bounds.right - _words[word].bounds.left,
-		yPos + _words[word].bounds.bottom - _words[word].bounds.top);
-	Common::Rect dstRect = _words[word].bounds;
-	dstRect.translate(_rect.left, _rect.top);
-	_vm->_gfx->copyAnimImageSectionToScreen(_resourceId, srcRect, dstRect);
-}
-
-void LBLiveTextItem::handleMouseDown(Common::Point pos) {
-	if (!_loaded || !_enabled || !_globalEnabled || _playing)
-		return LBItem::handleMouseDown(pos);
-
-	pos.x -= _rect.left;
-	pos.y -= _rect.top;
-
-	for (uint i = 0; i < _words.size(); i++) {
-		if (_words[i].bounds.contains(pos)) {
-			if (_currentWord != 0xFFFF) {
-				paletteUpdate(_currentWord, false);
-				_currentWord = 0xFFFF;
-			}
-			uint16 soundId = _words[i].soundId;
-			if (!soundId) {
-				// TODO: can we be smarter here, using timing?
-				warning("ignoring click due to no soundId");
-				return;
-			}
-			_currentWord = i;
-			_vm->playSound(this, soundId);
-			paletteUpdate(_currentWord, true);
-			return;
-		}
-	}
-
-	return LBItem::handleMouseDown(pos);
-}
-
-bool LBLiveTextItem::togglePlaying(bool playing, bool restart) {
-	if (!playing)
-		return LBItem::togglePlaying(playing, restart);
-	if (!_loaded || !_enabled || !_globalEnabled)
-		return _playing;
-
-	// TODO: handle this properly
-	_vm->_sound->stopSound();
-
-	_currentWord = 0xFFFF;
-	_currentPhrase = 0xFFFF;
-
-	return true;
-}
-
-void LBLiveTextItem::stop() {
-	// TODO: stop sound, refresh palette
-
-	LBItem::stop();
-}
-
-void LBLiveTextItem::notify(uint16 data, uint16 from) {
-	if (!_loaded || !_enabled || !_globalEnabled || !_playing)
-		return LBItem::notify(data, from);
-
-	if (_currentWord != 0xFFFF) {
-		// TODO: handle this properly
-		_vm->_sound->stopSound();
-		paletteUpdate(_currentWord, false);
-		_currentWord = 0xFFFF;
-	}
-
-	for (uint i = 0; i < _phrases.size(); i++) {
-		if (_phrases[i].highlightStart == data && _phrases[i].startId == from) {
-			debug(2, "Enabling phrase %d", i);
-			for (uint j = 0; j < _phrases[i].wordCount; j++) {
-				paletteUpdate(_phrases[i].wordStart + j, true);
-			}
-			_currentPhrase = i;
-			// TODO: not sure this is the correct logic
-			if (i == _phrases.size() - 1) {
-				_currentPhrase = 0xFFFF;
-				done(true);
-			}
-		} else if (_phrases[i].highlightEnd == data && _phrases[i].endId == from) {
-			debug(2, "Disabling phrase %d", i);
-			for (uint j = 0; j < _phrases[i].wordCount; j++) {
-				paletteUpdate(_phrases[i].wordStart + j, false);
-			}
-			_currentPhrase = 0xFFFF;
-		}
-	}
-
-	LBItem::notify(data, from);
-}
-
-LBItem *LBLiveTextItem::createClone() {
-	error("can't clone LBLiveTextItem");
-}
-
-LBPictureItem::LBPictureItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
-	debug(3, "new LBPictureItem");
-}
-
-void LBPictureItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
-	switch (type) {
-	case kLBSetDrawMode:
-		{
-		assert(size == 2);
-		// TODO: this probably sets whether points are always contained (0x10)
-		// or whether the bitmap contents are checked (00, or anything else?)
-		uint16 val = stream->readUint16();
-		debug(2, "LBPictureItem: kLBSetDrawMode: %04x", val);
-		}
-		break;
-
-	default:
-		LBItem::readData(type, size, stream);
-	}
-}
-
-bool LBPictureItem::contains(Common::Point point) {
-	if (!LBItem::contains(point))
-		return false;
-
-	if (!_doHitTest)
-		return true;
-
-	// TODO: only check pixels if necessary
-	return !_vm->_gfx->imageIsTransparentAt(_resourceId, false, point.x - _rect.left, point.y - _rect.top);
-}
-
-void LBPictureItem::init() {
-	_vm->_gfx->preloadImage(_resourceId);
-
-	LBItem::init();
-}
-
-void LBPictureItem::draw() {
-	if (!_loaded || !_visible || !_globalVisible)
-		return;
-
-	_vm->_gfx->copyAnimImageToScreen(_resourceId, _rect.left, _rect.top);
-}
-
-LBItem *LBPictureItem::createClone() {
-	return new LBPictureItem(_vm, _page, _rect);
-}
-
-LBAnimationItem::LBAnimationItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
-	_anim = nullptr;
-	_running = false;
-	debug(3, "new LBAnimationItem");
-}
-
-LBAnimationItem::~LBAnimationItem() {
-	delete _anim;
-}
-
-void LBAnimationItem::setEnabled(bool enabled) {
-	if (_running) {
-		if (enabled && _globalEnabled && !_loaded)
-			_anim->start();
-		else if (_loaded && !enabled && _enabled && _globalEnabled)
-			_anim->stop();
-	}
-
-	return LBItem::setEnabled(enabled);
-}
-
-bool LBAnimationItem::contains(Common::Point point) {
-	if (!LBItem::contains(point))
-		return false;
-
-	if (!_doHitTest)
-		return true;
-
-	return !_anim->transparentAt(point.x, point.y);
-}
-
-void LBAnimationItem::update() {
-	if (_loaded && _enabled && _globalEnabled && _running) {
-		bool wasDone = _anim->update();
-		if (wasDone) {
-			_running = false;
-			done(true);
-		}
-	}
-
-	LBItem::update();
-}
-
-bool LBAnimationItem::togglePlaying(bool playing, bool restart) {
-	if (playing) {
-		if (_loaded && _enabled && _globalEnabled) {
-			if (restart)
-				seek(1);
-			_running = true;
-			_anim->start();
-		}
-
-		return _running;
-	}
-
-	return LBItem::togglePlaying(playing, restart);
-}
-
-void LBAnimationItem::done(bool onlyNotify) {
-	if (!onlyNotify) {
-		_anim->stop();
-	}
-
-	LBItem::done(onlyNotify);
-}
-
-void LBAnimationItem::init() {
-	_anim = new LBAnimation(_vm, this, _resourceId);
-
-	LBItem::init();
-}
-
-void LBAnimationItem::stop() {
-	if (_running) {
-		_anim->stop();
-		seek(0xFFFF);
-	}
-
-	_running = false;
-
-	LBItem::stop();
-}
-
-void LBAnimationItem::seek(uint16 pos) {
-	_anim->seek(pos);
-}
-
-void LBAnimationItem::seekToTime(uint32 time) {
-	_anim->seekToTime(time);
-}
-
-void LBAnimationItem::startPhase(uint phase) {
-	if (phase == _phase)
-		seek(1);
-
-	LBItem::startPhase(phase);
-}
-
-void LBAnimationItem::draw() {
-	if (!_visible || !_globalVisible)
-		return;
-
-	_anim->draw();
-}
-
-LBItem *LBAnimationItem::createClone() {
-	LBAnimationItem *item = new LBAnimationItem(_vm, _page, _rect);
-	item->_anim = new LBAnimation(_vm, item, _resourceId);
-	return item;
-}
-
-LBMovieItem::LBMovieItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
-	debug(3, "new LBMovieItem");
-}
-
-LBMovieItem::~LBMovieItem() {
-}
-
-void LBMovieItem::update() {
-	if (_playing) {
-		VideoEntryPtr video = _vm->_video->findVideo(_resourceId);
-		if (!video || video->endOfVideo())
-			done(true);
-	}
-
-	LBItem::update();
-}
-
-bool LBMovieItem::togglePlaying(bool playing, bool restart) {
-	if (playing) {
-		if ((_loaded && _enabled && _globalEnabled) || _phase == kLBPhaseNone) {
-			debug("toggled video for phase %d", _phase);
-			VideoEntryPtr video = _vm->_video->playMovie(_resourceId);
-			if (!video)
-				error("Failed to open tMOV %d", _resourceId);
-
-			video->moveTo(_rect.left, _rect.top);
-			return true;
-		}
-	}
-
-	return LBItem::togglePlaying(playing, restart);
-}
-
-LBItem *LBMovieItem::createClone() {
-	return new LBMovieItem(_vm, _page, _rect);
-}
-
-LBMiniGameItem::LBMiniGameItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
-	debug(3, "new LBMiniGameItem");
-}
-
-LBMiniGameItem::~LBMiniGameItem() {
-}
-
-bool LBMiniGameItem::togglePlaying(bool playing, bool restart) {
-	// HACK: Since we don't support any of these hardcoded mini games yet,
-	// just skip to the most logical page. For optional minigames, this
-	// will return the player to the previous page. For mandatory minigames,
-	// this will send the player to the next page.
-
-	uint16 destPage = 0;
-	bool returnToMenu = false;
-
-	// Figure out what minigame we have and bring us back to a page where
-	// the player can continue
-	if (_desc == "Kitch")     // Green Eggs and Ham: Kitchen minigame
-		destPage = 4;
-	else if (_desc == "Eggs") // Green Eggs and Ham: Eggs minigame
-		destPage = 5;
-	else if (_desc == "Fall") // Green Eggs and Ham: Fall minigame
-		destPage = 13;
-	else if (_desc == "MagicWrite3") // Arthur's Reading Race: "Let Me Write" minigame (Page 3)
-		destPage = 3;
-	else if (_desc == "MagicWrite4") // Arthur's Reading Race: "Let Me Write" minigame (Page 4)
-		destPage = 4;
-	else if (_desc == "MagicSpy5") // Arthur's Reading Race: "I Spy" minigame (Page 5)
-		destPage = 5;
-	else if (_desc == "MagicSpy6") // Arthur's Reading Race: "I Spy" minigame (Page 6)
-		destPage = 6;
-	else if (_desc == "MagicWrite7") // Arthur's Reading Race: "Let Me Write" minigame (Page 7)
-		destPage = 7;
-	else if (_desc == "MagicSpy8") // Arthur's Reading Race: "I Spy" minigame (Page 8)
-		destPage = 8;
-	else if (_desc == "MagicRace") // Arthur's Reading Race: Race minigame
-		returnToMenu = true;
-	else
-		error("Unknown minigame '%s'", _desc.c_str());
-
-	GUI::MessageDialog dialog(Common::String::format("The '%s' minigame is not supported yet.", _desc.c_str()));
-	dialog.runModal();
-
-	// Go back to the menu if requested, otherwise go to the requested page
-	if (returnToMenu)
-		_vm->addNotifyEvent(NotifyEvent(kLBNotifyGoToControls, 1));
-	else
-		_vm->addNotifyEvent(NotifyEvent(kLBNotifyChangePage, destPage));
-
-	return false;
-}
-
-LBItem *LBMiniGameItem::createClone() {
-	error("can't clone LBMiniGameItem");
-}
-
-LBProxyItem::LBProxyItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
-	debug(3, "new LBProxyItem");
-
-	_page = nullptr;
-}
-
-LBProxyItem::~LBProxyItem() {
-	delete _page;
-}
-
-void LBProxyItem::load() {
-	if (_loaded)
-		return;
-
-	Common::String leftover;
-	Common::String filename = _vm->getFileNameFromConfig("Proxies", _desc.c_str(), leftover);
-	if (!leftover.empty())
-		error("LBProxyItem tried loading proxy '%s' but got leftover '%s'", _desc.c_str(), leftover.c_str());
-	uint16 baseId = 0;
-	for (uint i = 0; i < filename.size(); i++) {
-		if (filename[i] == ';') {
-			baseId = atoi(filename.c_str() + i + 1);
-			filename = Common::String(filename.c_str(), i);
-		}
-	}
-
-	debug(1, "LBProxyItem loading archive '%s' with id %d", filename.c_str(), baseId);
-	Archive *pageArchive = _vm->createArchive();
-	if (!tryOpenPage(pageArchive, filename))
-		error("failed to open archive '%s' (for proxy '%s')", filename.c_str(), _desc.c_str());
-	_page = new LBPage(_vm);
-	_page->open(pageArchive, baseId);
-
-	LBItem::load();
-}
-
-void LBProxyItem::unload() {
-	delete _page;
-	_page = nullptr;
-
-	LBItem::unload();
-}
-
-LBItem *LBProxyItem::createClone() {
-	return new LBProxyItem(_vm, _page, _rect);
-}
-
 } // End of namespace Mohawk
diff --git a/engines/mohawk/livingbooks_animation.cpp b/engines/mohawk/livingbooks_animation.cpp
new file mode 100644
index 00000000000..33c904c85fd
--- /dev/null
+++ b/engines/mohawk/livingbooks_animation.cpp
@@ -0,0 +1,704 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mohawk/livingbooks.h"
+#include "mohawk/resource.h"
+
+#include "common/system.h"
+#include "common/textconsole.h"
+
+namespace Mohawk {
+
+// SCRP - Animation sprites/background
+
+LBAnimationNode::LBAnimationNode(MohawkEngine_LivingBooks *vm, LBAnimation *parent, uint16 scriptResourceId) : _vm(vm), _parent(parent) {
+	loadScript(scriptResourceId);
+}
+
+LBAnimationNode::~LBAnimationNode() {
+	for (uint32 i = 0; i < _scriptEntries.size(); i++)
+		delete[] _scriptEntries[i].data;
+}
+
+void LBAnimationNode::loadScript(uint16 resourceId) {
+	Common::SeekableReadStreamEndian *scriptStream = _vm->wrapStreamEndian(ID_SCRP, resourceId);
+
+	reset();
+
+	while (byte opcodeId = scriptStream->readByte()) {
+		byte size = scriptStream->readByte();
+
+		LBAnimScriptEntry entry;
+		entry.opcode = opcodeId;
+		entry.size = size;
+
+		if (!size) {
+			entry.data = nullptr;
+		} else {
+			entry.data = new byte[entry.size];
+			scriptStream->read(entry.data, entry.size);
+		}
+
+		_scriptEntries.push_back(entry);
+	}
+
+	byte size = scriptStream->readByte();
+	if (size != 0 || scriptStream->pos() != scriptStream->size())
+		error("Failed to read script correctly");
+
+	delete scriptStream;
+}
+
+void LBAnimationNode::draw(const Common::Rect &_bounds) {
+	if (!_currentCel)
+		return;
+
+	// this is also checked in SetCel, below
+	if (_currentCel > _parent->getNumResources())
+		error("Animation cel %d was too high, this shouldn't happen!", _currentCel);
+
+	int16 xOffset = _xPos + _bounds.left;
+	int16 yOffset = _yPos + _bounds.top;
+
+	uint16 resourceId = _parent->getResource(_currentCel - 1);
+
+	if (!_vm->isPreMohawk()) {
+		Common::Point offset = _parent->getOffset(_currentCel - 1);
+		xOffset -= offset.x;
+		yOffset -= offset.y;
+	}
+
+	_vm->_gfx->copyOffsetAnimImageToScreen(resourceId, xOffset, yOffset);
+}
+
+void LBAnimationNode::reset() {
+	// TODO: this causes stupid flickering
+	//if (_currentCel)
+	//	_vm->_needsRedraw = true;
+
+	_currentCel = 0;
+	_currentEntry = 0;
+	_delay = 0;
+
+	_xPos = 0;
+	_yPos = 0;
+}
+
+NodeState LBAnimationNode::update(bool seeking) {
+	if (_currentEntry == _scriptEntries.size())
+		return kLBNodeDone;
+
+	if (_delay > 0 && --_delay)
+		return kLBNodeRunning;
+
+	while (_currentEntry < _scriptEntries.size()) {
+		LBAnimScriptEntry &entry = _scriptEntries[_currentEntry];
+		_currentEntry++;
+		debug(5, "Running script entry %d of %d", _currentEntry, _scriptEntries.size());
+
+		switch (entry.opcode) {
+		case kLBAnimOpPlaySound:
+		case kLBAnimOpWaitForSound:
+		case kLBAnimOpReleaseSound:
+		case kLBAnimOpResetSound:
+			{
+			uint16 soundResourceId = READ_BE_UINT16(entry.data);
+
+			if (!soundResourceId) {
+				error("Unhandled named wave file, tell clone2727 where you found this");
+				break;
+			}
+
+			Common::String cue;
+			uint pos = 2;
+			while (pos < entry.size) {
+				char in = entry.data[pos];
+				if (!in)
+					break;
+				pos++;
+				cue += in;
+			}
+			if (pos == entry.size)
+				error("Cue in sound kLBAnimOp wasn't null-terminated");
+
+			switch (entry.opcode) {
+			case kLBAnimOpPlaySound:
+				if (seeking)
+					break;
+				debug(4, "a: PlaySound(%0d)", soundResourceId);
+				_parent->playSound(soundResourceId);
+				break;
+			case kLBAnimOpWaitForSound:
+				if (seeking)
+					break;
+				debug(4, "b: WaitForSound(%0d)", soundResourceId);
+				if (!_parent->soundPlaying(soundResourceId, cue))
+					break;
+				_currentEntry--;
+				return kLBNodeWaiting;
+			case kLBAnimOpReleaseSound:
+				debug(4, "c: ReleaseSound(%0d)", soundResourceId);
+				// TODO
+				_vm->_sound->stopSound(soundResourceId);
+				break;
+			case kLBAnimOpResetSound:
+				debug(4, "d: ResetSound(%0d)", soundResourceId);
+				// TODO
+				_vm->_sound->stopSound(soundResourceId);
+				break;
+			default:
+				break;
+			}
+			}
+			break;
+
+		case kLBAnimOpSetTempo:
+		case kLBAnimOpSetTempoDiv:
+			{
+			assert(entry.size == 2);
+			uint16 tempo = (int16)READ_BE_UINT16(entry.data);
+
+			// TODO: LB 3 uses fixed-point here.
+			if (entry.opcode == kLBAnimOpSetTempo) {
+				debug(4, "3: SetTempo(%d)", tempo);
+				// TODO: LB 3 uses (tempo * 1000) / 60, while
+				// the original divides the system time by 16.
+				_parent->setTempo(tempo * 16);
+			} else {
+				// LB 3.0+ only.
+				debug(4, "E: SetTempoDiv(%d)", tempo);
+				_parent->setTempo(1000 / tempo);
+			}
+
+			}
+			break;
+
+		case kLBAnimOpWait:
+			assert(entry.size == 0);
+			debug(5, "6: Wait()");
+			return kLBNodeRunning;
+
+		case kLBAnimOpMoveTo:
+			{
+			assert(entry.size == 4);
+			int16 x = (int16)READ_BE_UINT16(entry.data);
+			int16 y = (int16)READ_BE_UINT16(entry.data + 2);
+			debug(4, "5: MoveTo(%d, %d)", x, y);
+
+			_xPos = x;
+			_yPos = y;
+			_vm->_needsRedraw = true;
+			}
+			break;
+
+		case kLBAnimOpDrawMode:
+			{
+			assert(entry.size == 2);
+			uint16 mode = (int16)READ_BE_UINT16(entry.data);
+			debug(4, "9: DrawMode(%d)", mode);
+
+			// TODO
+			}
+			break;
+
+		case kLBAnimOpSetCel:
+			{
+			assert(entry.size == 2);
+			uint16 cel = (int16)READ_BE_UINT16(entry.data);
+			debug(4, "7: SetCel(%d)", cel);
+
+			_currentCel = cel;
+			if (_currentCel > _parent->getNumResources())
+				error("SetCel set current cel to %d, but we only have %d cels", _currentCel, _parent->getNumResources());
+			_vm->_needsRedraw = true;
+			}
+			break;
+
+		case kLBAnimOpNotify:
+			{
+			assert(entry.size == 2);
+			uint16 data = (int16)READ_BE_UINT16(entry.data);
+
+			if (seeking)
+				break;
+
+			debug(4, "2: Notify(%d)", data);
+			_vm->notifyAll(data, _parent->getParentId());
+			}
+			break;
+
+		case kLBAnimOpSleepUntil:
+			{
+			assert(entry.size == 4);
+			uint32 frame = READ_BE_UINT32(entry.data);
+			debug(4, "8: SleepUntil(%d)", frame);
+
+			if (frame > _parent->getCurrentFrame()) {
+				// *not* kLBNodeWaiting
+				_currentEntry--;
+				return kLBNodeRunning;
+			}
+			}
+			break;
+
+		case kLBAnimOpDelay:
+			{
+			assert(entry.size == 4);
+			uint32 delay = READ_BE_UINT32(entry.data);
+			debug(4, "f: Delay(%d)", delay);
+			_delay = delay;
+			return kLBNodeRunning;
+			}
+			break;
+
+		default:
+			error("Unknown opcode id %02x (size %d)", entry.opcode, entry.size);
+			break;
+		}
+	}
+
+	return kLBNodeRunning;
+}
+
+bool LBAnimationNode::transparentAt(int x, int y) {
+	if (!_currentCel)
+		return true;
+
+	uint16 resourceId = _parent->getResource(_currentCel - 1);
+
+	if (!_vm->isPreMohawk()) {
+		Common::Point offset = _parent->getOffset(_currentCel - 1);
+		x += offset.x;
+		y += offset.y;
+	}
+
+	// TODO: only check pixels if necessary
+	return _vm->_gfx->imageIsTransparentAt(resourceId, true, x - _xPos, y - _yPos);
+}
+
+LBAnimation::LBAnimation(MohawkEngine_LivingBooks *vm, LBAnimationItem *parent, uint16 resourceId) : _vm(vm), _parent(parent) {
+	Common::SeekableReadStreamEndian *aniStream = _vm->wrapStreamEndian(ID_ANI, resourceId);
+
+	// ANI records in the Wanderful sampler are 32 bytes, extra bytes are just NULs
+	if (aniStream->size() != 30 && aniStream->size() != 32)
+		warning("ANI Record size mismatch");
+
+	uint16 version = aniStream->readUint16();
+	if (version != 1)
+		warning("ANI version not 1");
+
+	_bounds = _vm->readRect(aniStream);
+	_clip = _vm->readRect(aniStream);
+	// TODO: what is colorId for?
+	uint32 colorId = aniStream->readUint32();
+	uint32 sprResourceId = aniStream->readUint32();
+	uint32 sprResourceOffset = aniStream->readUint32();
+
+	debug(5, "ANI bounds: (%d, %d), (%d, %d)", _bounds.left, _bounds.top, _bounds.right, _bounds.bottom);
+	debug(5, "ANI clip: (%d, %d), (%d, %d)", _clip.left, _clip.top, _clip.right, _clip.bottom);
+	debug(5, "ANI color id: %d", colorId);
+	debug(5, "ANI SPRResourceId: %d, offset %d", sprResourceId, sprResourceOffset);
+	if (aniStream->size() == 32) {
+		debug(5, "ANI extra bytes: (%d)", aniStream->readUint16());
+	}
+
+	if (aniStream->pos() != aniStream->size())
+		error("Still %d bytes at the end of anim stream", (int)(aniStream->size() - aniStream->pos()));
+
+	delete aniStream;
+
+	if (sprResourceOffset)
+		error("Cannot handle non-zero ANI offset yet");
+
+	Common::SeekableReadStreamEndian *sprStream = _vm->wrapStreamEndian(ID_SPR, sprResourceId);
+
+	uint16 numBackNodes = sprStream->readUint16();
+	uint16 numFrontNodes = sprStream->readUint16();
+	uint32 shapeResourceID = sprStream->readUint32();
+	uint32 shapeResourceOffset = sprStream->readUint32();
+	uint32 scriptResourceID = sprStream->readUint32();
+	uint32 scriptResourceOffset = sprStream->readUint32();
+	uint32 scriptResourceLength = sprStream->readUint32();
+	debug(5, "SPR# stream: %d front, %d background", numFrontNodes, numBackNodes);
+	debug(5, "Shape ID %d (offset 0x%04x), script ID %d (offset 0x%04x, length %d)", shapeResourceID, shapeResourceOffset,
+		scriptResourceID, scriptResourceOffset, scriptResourceLength);
+
+	Common::Array<uint16> scriptIDs;
+	for (uint16 i = 0; i < numFrontNodes; i++) {
+		uint32 unknown1 = sprStream->readUint32();
+		uint32 unknown2 = sprStream->readUint32();
+		uint32 unknown3 = sprStream->readUint32();
+		uint16 scriptID = sprStream->readUint32();
+		uint32 unknown4 = sprStream->readUint32();
+		uint32 unknown5 = sprStream->readUint32();
+		scriptIDs.push_back(scriptID);
+		debug(6, "Front node %d: script ID %d", i, scriptID);
+		if (unknown1 != 0 || unknown2 != 0 || unknown3 != 0 || unknown4 != 0 || unknown5 != 0)
+			error("Anim node %d had non-zero unknowns %08x, %08x, %08x, %08x, %08x",
+				i, unknown1, unknown2, unknown3, unknown4, unknown5);
+	}
+
+	if (numBackNodes)
+		error("Ignoring %d back nodes", numBackNodes);
+
+	if (sprStream->pos() != sprStream->size())
+		error("Still %d bytes at the end of sprite stream", (int)(sprStream->size() - sprStream->pos()));
+
+	delete sprStream;
+
+	loadShape(shapeResourceID);
+
+	_nodes.push_back(new LBAnimationNode(_vm, this, scriptResourceID));
+	for (uint16 i = 0; i < scriptIDs.size(); i++)
+		_nodes.push_back(new LBAnimationNode(_vm, this, scriptIDs[i]));
+
+	_currentFrame = 0;
+	_currentSound = 0xffff;
+	_running = false;
+	_tempo = 1;
+}
+
+LBAnimation::~LBAnimation() {
+	for (uint32 i = 0; i < _nodes.size(); i++)
+		delete _nodes[i];
+	if (_currentSound != 0xffff)
+		_vm->_sound->stopSound(_currentSound);
+}
+
+void LBAnimation::loadShape(uint16 resourceId) {
+	if (resourceId == 0)
+		return;
+
+	Common::SeekableReadStreamEndian *shapeStream = _vm->wrapStreamEndian(ID_SHP, resourceId);
+
+	if (_vm->isPreMohawk()) {
+		if (shapeStream->size() < 6)
+			error("V1 SHP Record size too short (%d)", (int)shapeStream->size());
+
+		uint16 u0 = shapeStream->readUint16();
+		if (u0 != 3)
+			error("V1 SHP Record u0 is %04x, not 3", u0);
+
+		uint16 u1 = shapeStream->readUint16();
+		if (u1 != 0)
+			error("V1 SHP Record u1 is %04x, not 0", u1);
+
+		uint16 idCount = shapeStream->readUint16();
+		debug(8, "V1 SHP: idCount: %d", idCount);
+
+		if (shapeStream->size() != (idCount * 2) + 6)
+			error("V1 SHP Record size mismatch (%d)", (int)shapeStream->size());
+
+		for (uint16 i = 0; i < idCount; i++) {
+			_shapeResources.push_back(shapeStream->readUint16());
+			debug(8, "V1 SHP: BMAP Resource Id %d: %d", i, _shapeResources[i]);
+		}
+	} else {
+		uint16 idCount = shapeStream->readUint16();
+		debug(8, "SHP: idCount: %d", idCount);
+
+		if (shapeStream->size() != (idCount * 6) + 2)
+			error("SHP Record size mismatch (%d)", (int)shapeStream->size());
+
+		for (uint16 i = 0; i < idCount; i++) {
+			_shapeResources.push_back(shapeStream->readUint16());
+			int16 x = shapeStream->readSint16();
+			int16 y = shapeStream->readSint16();
+			_shapeOffsets.push_back(Common::Point(x, y));
+			debug(8, "SHP: tBMP Resource Id %d: %d, at (%d, %d)", i, _shapeResources[i], x, y);
+		}
+	}
+
+	for (uint16 i = 0; i < _shapeResources.size(); i++)
+		_vm->_gfx->preloadImage(_shapeResources[i]);
+
+	delete shapeStream;
+}
+
+void LBAnimation::draw() {
+	for (uint32 i = 0; i < _nodes.size(); i++)
+		_nodes[i]->draw(_bounds);
+}
+
+bool LBAnimation::update() {
+	if (!_running)
+		return false;
+
+	if (_vm->_system->getMillis() <= _lastTime + (uint32)_tempo)
+		return false;
+
+	// the second check is to try 'catching up' with lagged animations, might be crazy
+	if (_lastTime == 0 || (_vm->_system->getMillis()) > _lastTime + (uint32)(_tempo * 2))
+		_lastTime = _vm->_system->getMillis();
+	else
+		_lastTime += _tempo;
+
+	if (_currentSound != 0xffff && !_vm->_sound->isPlaying(_currentSound)) {
+		_currentSound = 0xffff;
+	}
+
+	NodeState state = kLBNodeDone;
+	for (uint32 i = 0; i < _nodes.size(); i++) {
+		NodeState s = _nodes[i]->update();
+		if (s == kLBNodeWaiting) {
+			state = kLBNodeWaiting;
+			if (i != 0)
+				warning("non-primary node was waiting");
+			break;
+		}
+		if (s == kLBNodeRunning)
+			state = kLBNodeRunning;
+	}
+
+	if (state == kLBNodeRunning) {
+		_currentFrame++;
+	} else if (state == kLBNodeDone) {
+		if (_currentSound == 0xffff) {
+			_running = false;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+void LBAnimation::start() {
+	_lastTime = 0;
+	_running = true;
+}
+
+void LBAnimation::seek(uint16 pos) {
+	_lastTime = 0;
+	_currentFrame = 0;
+
+	if (_currentSound != 0xffff) {
+		_vm->_sound->stopSound(_currentSound);
+		_currentSound = 0xffff;
+	}
+
+	for (uint32 i = 0; i < _nodes.size(); i++)
+		_nodes[i]->reset();
+
+	for (uint16 n = 0; n < pos; n++) {
+		bool ranSomething = false;
+		// nodes don't wait while seeking
+		for (uint32 i = 0; i < _nodes.size(); i++)
+			ranSomething |= (_nodes[i]->update(true) != kLBNodeDone);
+
+		_currentFrame++;
+
+		if (!ranSomething) {
+			_running = false;
+			break;
+		}
+	}
+}
+
+void LBAnimation::seekToTime(uint32 time) {
+	_lastTime = 0;
+	_currentFrame = 0;
+
+	if (_currentSound != 0xffff) {
+		_vm->_sound->stopSound(_currentSound);
+		_currentSound = 0xffff;
+	}
+
+	for (uint32 i = 0; i < _nodes.size(); i++)
+		_nodes[i]->reset();
+
+	uint32 elapsed = 0;
+	while (elapsed <= time) {
+		bool ranSomething = false;
+		// nodes don't wait while seeking
+		for (uint32 i = 0; i < _nodes.size(); i++)
+			ranSomething |= (_nodes[i]->update(true) != kLBNodeDone);
+
+		elapsed += _tempo;
+		_currentFrame++;
+
+		if (!ranSomething) {
+			_running = false;
+			break;
+		}
+	}
+}
+
+void LBAnimation::stop() {
+	_running = false;
+	if (_currentSound != 0xffff) {
+		_vm->_sound->stopSound(_currentSound);
+		_currentSound = 0xffff;
+	}
+}
+
+void LBAnimation::playSound(uint16 resourceId) {
+	_currentSound = resourceId;
+	_vm->_sound->playSound(_currentSound, Audio::Mixer::kMaxChannelVolume, false, &_cueList);
+}
+
+bool LBAnimation::soundPlaying(uint16 resourceId, const Common::String &cue) {
+	if (_currentSound != resourceId)
+		return false;
+	if (!_vm->_sound->isPlaying(_currentSound))
+		return false;
+
+	if (cue.empty())
+		return true;
+
+	uint samples = _vm->_sound->getNumSamplesPlayed(_currentSound);
+	for (uint i = 0; i < _cueList.pointCount; i++) {
+		if (_cueList.points[i].sampleFrame > samples)
+			break;
+		if (_cueList.points[i].name == cue)
+			return false;
+	}
+
+	return true;
+}
+
+bool LBAnimation::transparentAt(int x, int y) {
+	for (uint32 i = 0; i < _nodes.size(); i++)
+		if (!_nodes[i]->transparentAt(x - _bounds.left, y - _bounds.top))
+			return false;
+
+	return true;
+}
+
+void LBAnimation::setTempo(uint16 tempo) {
+	_tempo = tempo;
+}
+
+uint16 LBAnimation::getParentId() {
+	return _parent->getId();
+}
+
+LBAnimationItem::LBAnimationItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
+	_anim = nullptr;
+	_running = false;
+	debug(3, "new LBAnimationItem");
+}
+
+LBAnimationItem::~LBAnimationItem() {
+	delete _anim;
+}
+
+void LBAnimationItem::setEnabled(bool enabled) {
+	if (_running) {
+		if (enabled && _globalEnabled && !_loaded)
+			_anim->start();
+		else if (_loaded && !enabled && _enabled && _globalEnabled)
+			_anim->stop();
+	}
+
+	return LBItem::setEnabled(enabled);
+}
+
+bool LBAnimationItem::contains(Common::Point point) {
+	if (!LBItem::contains(point))
+		return false;
+
+	if (!_doHitTest)
+		return true;
+
+	return !_anim->transparentAt(point.x, point.y);
+}
+
+void LBAnimationItem::update() {
+	if (_loaded && _enabled && _globalEnabled && _running) {
+		bool wasDone = _anim->update();
+		if (wasDone) {
+			_running = false;
+			done(true);
+		}
+	}
+
+	LBItem::update();
+}
+
+bool LBAnimationItem::togglePlaying(bool playing, bool restart) {
+	if (playing) {
+		if (_loaded && _enabled && _globalEnabled) {
+			if (restart)
+				seek(1);
+			_running = true;
+			_anim->start();
+		}
+
+		return _running;
+	}
+
+	return LBItem::togglePlaying(playing, restart);
+}
+
+void LBAnimationItem::done(bool onlyNotify) {
+	if (!onlyNotify) {
+		_anim->stop();
+	}
+
+	LBItem::done(onlyNotify);
+}
+
+void LBAnimationItem::init() {
+	_anim = new LBAnimation(_vm, this, _resourceId);
+
+	LBItem::init();
+}
+
+void LBAnimationItem::stop() {
+	if (_running) {
+		_anim->stop();
+		seek(0xFFFF);
+	}
+
+	_running = false;
+
+	LBItem::stop();
+}
+
+void LBAnimationItem::seek(uint16 pos) {
+	_anim->seek(pos);
+}
+
+void LBAnimationItem::seekToTime(uint32 time) {
+	_anim->seekToTime(time);
+}
+
+void LBAnimationItem::startPhase(uint phase) {
+	if (phase == _phase)
+		seek(1);
+
+	LBItem::startPhase(phase);
+}
+
+void LBAnimationItem::draw() {
+	if (!_visible || !_globalVisible)
+		return;
+
+	_anim->draw();
+}
+
+LBItem *LBAnimationItem::createClone() {
+	LBAnimationItem *item = new LBAnimationItem(_vm, _page, _rect);
+	item->_anim = new LBAnimation(_vm, item, _resourceId);
+	return item;
+}
+
+} // End of namespace Mohawk
diff --git a/engines/mohawk/livingbooks_bookinfo.cpp b/engines/mohawk/livingbooks_bookinfo.cpp
new file mode 100644
index 00000000000..1e7988016a4
--- /dev/null
+++ b/engines/mohawk/livingbooks_bookinfo.cpp
@@ -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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mohawk/livingbooks.h"
+
+#include "common/textconsole.h"
+
+namespace Mohawk {
+
+void MohawkEngine_LivingBooks::loadBookInfo(const Common::Path &filename) {
+	_bookInfoFile.allowNonEnglishCharacters();
+	if (!_bookInfoFile.loadFromFile(filename))
+		error("Could not open %s as a config file", filename.toString().c_str());
+
+	_title = getStringFromConfig("BookInfo", "title");
+	_copyright = getStringFromConfig("BookInfo", "copyright");
+
+	_numPages = getIntFromConfig("BookInfo", "nPages");
+	_numLanguages = getIntFromConfig("BookInfo", "nLanguages");
+	_screenWidth = getIntFromConfig("BookInfo", "xRes");
+	_screenHeight = getIntFromConfig("BookInfo", "yRes");
+	// nColors is here too, but it's always 256 anyway...
+
+	// this is 1 in The New Kid on the Block, changes the hardcoded UI
+	// v2 games changed the flag name to fPoetry
+	if (getGameType() == GType_LIVINGBOOKSV1)
+		_poetryMode = (getIntFromConfig("BookInfo", "poetry") == 1);
+	else
+		_poetryMode = (getIntFromConfig("BookInfo", "fPoetry") == 1);
+
+	// The later Living Books games add some more options:
+	//     - fNeedPalette                (always true?)
+	//     - fUse254ColorPalette         (always true?)
+	//     - nKBRequired                 (4096, RAM requirement?)
+	//     - fDebugWindow                (always 0?)
+
+	if (_bookInfoFile.hasSection("Globals")) {
+		const Common::INIFile::SectionKeyList globals = _bookInfoFile.getKeys("Globals");
+		for (Common::INIFile::SectionKeyList::const_iterator i = globals.begin(); i != globals.end(); i++) {
+			Common::String command = Common::String::format("%s = %s", i->key.c_str(), i->value.c_str());
+			LBCode tempCode(this, 0);
+			uint offset = tempCode.parseCode(command);
+			tempCode.runCode(nullptr, offset);
+		}
+	}
+}
+
+Common::String MohawkEngine_LivingBooks::stringForMode(LBMode mode) {
+	Common::String language = getStringFromConfig("Languages", Common::String::format("Language%d", _curLanguage));
+
+	switch (mode) {
+	case kLBIntroMode:
+		return "Intro";
+	case kLBControlMode:
+		return "Control";
+	case kLBCreditsMode:
+		return "Credits";
+	case kLBPreviewMode:
+		return "Preview";
+	case kLBReadMode:
+		return language + ".Read";
+	case kLBPlayMode:
+		return language + ".Play";
+	default:
+		error("unknown game mode %d", (int)mode);
+	}
+}
+
+Common::String MohawkEngine_LivingBooks::getStringFromConfig(const Common::String &section, const Common::String &key) {
+	Common::String x, leftover;
+	_bookInfoFile.getKey(key, section, x);
+	Common::String tmp = removeQuotesFromString(x, leftover);
+	if (!leftover.empty())
+		warning("while parsing config key '%s' from section '%s', string '%s' was left after '%s'",
+			key.c_str(), section.c_str(), leftover.c_str(), tmp.c_str());
+	return tmp;
+}
+
+Common::String MohawkEngine_LivingBooks::getStringFromConfig(const Common::String &section, const Common::String &key, Common::String &leftover) {
+	Common::String x;
+	_bookInfoFile.getKey(key, section, x);
+	return removeQuotesFromString(x, leftover);
+}
+
+int MohawkEngine_LivingBooks::getIntFromConfig(const Common::String &section, const Common::String &key) {
+	return atoi(getStringFromConfig(section, key).c_str());
+}
+
+Common::String MohawkEngine_LivingBooks::getFileNameFromConfig(const Common::String &section, const Common::String &key, Common::String &leftover) {
+	Common::String string = getStringFromConfig(section, key, leftover);
+
+	if (string.hasPrefix("//")) {
+		// skip "//CD-ROM Title/" prefixes which we don't care about
+		uint i = 3;
+		while (i < string.size() && string[i - 1] != '/')
+			i++;
+
+		// Already uses slashes, no need to convert further
+		return string.c_str() + i;
+	}
+
+	return (getPlatform() == Common::kPlatformMacintosh) ? convertMacFileName(string) : convertWinFileName(string);
+}
+
+Common::String MohawkEngine_LivingBooks::removeQuotesFromString(const Common::String &string, Common::String &leftover) {
+	if (string.empty())
+		return string;
+
+	char quoteChar = string[0];
+	if (quoteChar != '\"' && quoteChar != '\'')
+		return string;
+
+	Common::String tmp;
+	bool inLeftover = false;
+	for (uint32 i = 1; i < string.size(); i++) {
+		if (inLeftover)
+			leftover += string[i];
+		else if (string[i] == quoteChar)
+			inLeftover = true;
+		else
+			tmp += string[i];
+	}
+
+	return tmp;
+}
+
+Common::String MohawkEngine_LivingBooks::convertMacFileName(const Common::String &string) {
+	Common::String filename;
+
+	for (uint32 i = 0; i < string.size(); i++) {
+		if (i == 0 && string[i] == ':') // First character should be ignored (another colon)
+			continue;
+		if (string[i] == ':') // Directory separator
+			filename += '/';
+		else if (string[i] == '/') // Literal slash
+			filename += ':'; // Replace by colon, as used by macOS for slash
+		else
+			filename += string[i];
+	}
+
+	return filename;
+}
+
+Common::String MohawkEngine_LivingBooks::convertWinFileName(const Common::String &string) {
+	Common::String filename;
+
+	for (uint32 i = 0; i < string.size(); i++) {
+		if (i == 0 && (string[i] == '/' || string[i] == '\\')) // ignore slashes at start
+			continue;
+		if (string[i] == '\\')
+			filename += '/';
+		else
+			filename += string[i];
+	}
+
+	return filename;
+}
+
+} // End of namespace Mohawk
diff --git a/engines/mohawk/livingbooks_itemtypes.cpp b/engines/mohawk/livingbooks_itemtypes.cpp
new file mode 100644
index 00000000000..b1ee4d10772
--- /dev/null
+++ b/engines/mohawk/livingbooks_itemtypes.cpp
@@ -0,0 +1,778 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mohawk/livingbooks.h"
+#include "mohawk/resource.h"
+#include "mohawk/video.h"
+
+#include "common/memstream.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "graphics/paletteman.h"
+
+#include "gui/message.h"
+
+namespace Mohawk {
+
+LBSoundItem::LBSoundItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
+	debug(3, "new LBSoundItem");
+	_running = false;
+}
+
+LBSoundItem::~LBSoundItem() {
+	if (_running)
+		_vm->_sound->stopSound(_resourceId);
+}
+
+void LBSoundItem::update() {
+	if (_running && !_vm->_sound->isPlaying(_resourceId)) {
+		_running = false;
+		done(true);
+	}
+
+	LBItem::update();
+}
+
+bool LBSoundItem::togglePlaying(bool playing, bool restart) {
+	if (!playing)
+		return LBItem::togglePlaying(playing, restart);
+
+	if (_running) {
+		_running = false;
+		_vm->_sound->stopSound(_resourceId);
+	}
+
+	if (!_loaded || !_enabled || !_globalEnabled)
+		return false;
+
+	_running = true;
+	debug(4, "sound %d play for item %d (%s)", _resourceId, _itemId, _desc.c_str());
+	_vm->playSound(this, _resourceId);
+	return true;
+}
+
+void LBSoundItem::stop() {
+	if (_running) {
+		_running = false;
+		_vm->_sound->stopSound(_resourceId);
+	}
+
+	LBItem::stop();
+}
+
+LBItem *LBSoundItem::createClone() {
+	return new LBSoundItem(_vm, _page, _rect);
+}
+
+LBGroupItem::LBGroupItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
+	debug(3, "new LBGroupItem");
+	_starting = false;
+}
+
+void LBGroupItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
+	switch (type) {
+	case kLBGroupData:
+		{
+		_groupEntries.clear();
+		uint16 count = stream->readUint16();
+		debug(3, "Group data: %d entries", count);
+
+		if (size != 2 + count * 4)
+			error("kLBGroupData was wrong size (%d, for %d entries)", size, count);
+
+		for (uint i = 0; i < count; i++) {
+			GroupEntry entry;
+			// TODO: is type important for any game? at the moment, we ignore it
+			entry.entryType = stream->readUint16();
+			entry.entryId = stream->readUint16();
+			// HACK: The Living Books v3 sampler includes the ID for the group as
+			// one of the entries in the Green Eggs and Ham section, which leads
+			// to infinite recursion when the group is loaded.
+			if (entry.entryId != getId())
+				_groupEntries.push_back(entry);
+			debug(3, "group entry: id %d, type %d", entry.entryId, entry.entryType);
+		}
+		}
+		break;
+
+	default:
+		LBItem::readData(type, size, stream);
+	}
+}
+
+void LBGroupItem::destroySelf() {
+	LBItem::destroySelf();
+
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->destroySelf();
+	}
+}
+
+void LBGroupItem::setEnabled(bool enabled) {
+	if (_starting) {
+		_starting = false;
+		LBItem::setEnabled(enabled);
+	} else {
+		for (uint i = 0; i < _groupEntries.size(); i++) {
+			LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+			if (item)
+				item->setEnabled(enabled);
+		}
+	}
+}
+
+void LBGroupItem::setGlobalEnabled(bool enabled) {
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->setGlobalEnabled(enabled);
+	}
+}
+
+bool LBGroupItem::contains(Common::Point point) {
+	return false;
+}
+
+bool LBGroupItem::togglePlaying(bool playing, bool restart) {
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->togglePlaying(playing, restart);
+	}
+
+	return false;
+}
+
+void LBGroupItem::seek(uint16 pos) {
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->seek(pos);
+	}
+}
+
+void LBGroupItem::setVisible(bool visible) {
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->setVisible(visible);
+	}
+}
+
+void LBGroupItem::setGlobalVisible(bool visible) {
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->setGlobalVisible(visible);
+	}
+}
+
+void LBGroupItem::startPhase(uint phase) {
+	_starting = true;
+	LBItem::startPhase(phase);
+	_starting = false;
+}
+
+void LBGroupItem::stop() {
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->stop();
+	}
+}
+
+void LBGroupItem::load() {
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->load();
+	}
+}
+
+void LBGroupItem::unload() {
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->unload();
+	}
+}
+
+void LBGroupItem::moveBy(const Common::Point &pos) {
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->moveBy(pos);
+	}
+}
+
+void LBGroupItem::moveTo(const Common::Point &pos) {
+	for (uint i = 0; i < _groupEntries.size(); i++) {
+		LBItem *item = _vm->getItemById(_groupEntries[i].entryId);
+		if (item)
+			item->moveTo(pos);
+	}
+}
+
+LBItem *LBGroupItem::createClone() {
+	// TODO: needed?
+	error("LBGroupItem::createClone unimplemented");
+	return new LBGroupItem(_vm, _page, _rect);
+}
+
+LBPaletteItem::LBPaletteItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
+	debug(3, "new LBPaletteItem");
+
+	_fadeInStart = 0;
+	_palette = nullptr;
+}
+
+LBPaletteItem::~LBPaletteItem() {
+	delete[] _palette;
+}
+
+void LBPaletteItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
+	switch (type) {
+	case kLBPaletteXData:
+		{
+		assert(size >= 8);
+		_fadeInPeriod = stream->readUint16();
+		_fadeInStep = stream->readUint16();
+		_drawStart = stream->readUint16();
+		_drawCount = stream->readUint16();
+		if (_drawStart + _drawCount > 256)
+			error("encountered palette trying to set more than 256 colors");
+		assert(size == 8 + _drawCount * 4);
+
+		// TODO: _drawCount is really more like _drawEnd, so once we're sure that
+		// there's really no use for the palette entries before _drawCount, we
+		// might want to just discard them here, at load time.
+		_palette = new byte[_drawCount * 3];
+		for (uint i = 0; i < _drawCount; i++) {
+			_palette[i*3 + 0] = stream->readByte();
+			_palette[i*3 + 1] = stream->readByte();
+			_palette[i*3 + 2] = stream->readByte();
+			stream->readByte();
+		}
+		}
+		break;
+
+	default:
+		LBItem::readData(type, size, stream);
+	}
+}
+
+bool LBPaletteItem::togglePlaying(bool playing, bool restart) {
+	// TODO: this likely isn't the right place
+
+	if (playing) {
+		_fadeInStart = _vm->_system->getMillis();
+		_fadeInCurrent = 0;
+
+		return true;
+	}
+
+	return LBItem::togglePlaying(playing, restart);
+}
+
+void LBPaletteItem::update() {
+	if (_fadeInStart) {
+		if (!_palette)
+			error("LBPaletteItem had no palette on startup");
+
+		uint32 elapsedTime = _vm->_system->getMillis() - _fadeInStart;
+		uint32 divTime = elapsedTime / _fadeInStep;
+
+		if (divTime > _fadeInPeriod)
+			divTime = _fadeInPeriod;
+
+		if (_fadeInCurrent != divTime) {
+			_fadeInCurrent = divTime;
+
+			// TODO: actual fading-in
+			if (_visible && _globalVisible) {
+				_vm->_system->getPaletteManager()->setPalette(_palette + _drawStart * 3, _drawStart, _drawCount - _drawStart);
+				_vm->_needsRedraw = true;
+			}
+		}
+
+		if (elapsedTime >= (uint32)_fadeInPeriod * (uint32)_fadeInStep) {
+			// TODO: correct?
+			_fadeInStart = 0;
+		}
+	}
+
+	LBItem::update();
+}
+
+LBItem *LBPaletteItem::createClone() {
+	error("can't clone LBPaletteItem");
+}
+
+LBLiveTextItem::LBLiveTextItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
+	_currentPhrase = 0xFFFF;
+	_currentWord = 0xFFFF;
+	debug(3, "new LBLiveTextItem");
+}
+
+void LBLiveTextItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
+	switch (type) {
+	case kLBLiveTextData:
+		{
+		stream->read(_backgroundColor, 4); // unused?
+		stream->read(_foregroundColor, 4);
+		stream->read(_highlightColor, 4);
+		_paletteIndex = stream->readUint16();
+		uint16 phraseCount = stream->readUint16();
+		uint16 wordCount = stream->readUint16();
+
+		debug(3, "LiveText has %d words in %d phrases, palette index 0x%04x", wordCount, phraseCount, _paletteIndex);
+		debug(3, "LiveText colors: background %02x%02x%02x%02x, foreground %02x%02x%02x%02x, highlight %02x%02x%02x%02x",
+			_backgroundColor[0], _backgroundColor[1], _backgroundColor[2], _backgroundColor[3],
+			_foregroundColor[0], _foregroundColor[1], _foregroundColor[2], _foregroundColor[3],
+			_highlightColor[0], _highlightColor[1], _highlightColor[2], _highlightColor[3]);
+
+		if (size != 18 + 14 * wordCount + 18 * phraseCount)
+			error("Bad Live Text data size (got %d, wanted %d words and %d phrases)", size, wordCount, phraseCount);
+
+		_words.clear();
+		for (uint i = 0; i < wordCount; i++) {
+			LiveTextWord word;
+			word.bounds = _vm->readRect(stream);
+			word.soundId = stream->readUint16();
+			word.itemType = stream->readUint16();
+			word.itemId = stream->readUint16();
+			debug(4, "Word: (%d, %d) to (%d, %d), sound %d, item %d (type %d)",
+				word.bounds.left, word.bounds.top, word.bounds.right, word.bounds.bottom, word.soundId, word.itemId, word.itemType);
+			_words.push_back(word);
+		}
+
+		_phrases.clear();
+		for (uint i = 0; i < phraseCount; i++) {
+			LiveTextPhrase phrase;
+			phrase.wordStart = stream->readUint16();
+			phrase.wordCount = stream->readUint16();
+			phrase.highlightStart = stream->readUint16();
+			phrase.startId = stream->readUint16();
+			phrase.highlightEnd = stream->readUint16();
+			phrase.endId = stream->readUint16();
+
+			// The original stored the values in uint32's so we need to swap here
+			if (_vm->isBigEndian()) {
+				SWAP(phrase.highlightStart, phrase.startId);
+				SWAP(phrase.highlightEnd, phrase.endId);
+			}
+
+			uint32 unknown1 = stream->readUint16();
+			uint16 unknown2 = stream->readUint32();
+
+			if (unknown1 != 0 || unknown2 != 0)
+				error("Unexpected unknowns %08x/%04x in LiveText word", unknown1, unknown2);
+
+			debug(4, "Phrase: start %d, count %d, start at %d (from %d), end at %d (from %d)",
+				phrase.wordStart, phrase.wordCount, phrase.highlightStart, phrase.startId, phrase.highlightEnd, phrase.endId);
+
+			_phrases.push_back(phrase);
+		}
+		}
+		break;
+
+	default:
+		LBItem::readData(type, size, stream);
+	}
+}
+
+bool LBLiveTextItem::contains(Common::Point point) {
+	if (!LBItem::contains(point))
+		return false;
+
+	point.x -= _rect.left;
+	point.y -= _rect.top;
+
+	for (uint i = 0; i < _words.size(); i++) {
+		if (_words[i].bounds.contains(point))
+			return true;
+	}
+
+	return false;
+}
+
+void LBLiveTextItem::paletteUpdate(uint16 word, bool on) {
+	_vm->_needsRedraw = true;
+
+	// Sometimes the last phrase goes out-of-bounds, the original engine
+	// only checks the words which are valid in the palette updating code.
+	if (word >= _words.size())
+		return;
+
+	if (_resourceId) {
+		// with a resource, we draw a bitmap in draw() rather than changing the palette
+		return;
+	}
+
+	if (on) {
+		_vm->_system->getPaletteManager()->setPalette(_highlightColor, _paletteIndex + word, 1);
+	} else {
+		_vm->_system->getPaletteManager()->setPalette(_foregroundColor, _paletteIndex + word, 1);
+	}
+}
+
+void LBLiveTextItem::update() {
+	if (_currentWord != 0xFFFF) {
+		uint16 soundId = _words[_currentWord].soundId;
+		if (soundId && !_vm->_sound->isPlaying(soundId)) {
+			paletteUpdate(_currentWord, false);
+
+			// TODO: check this in RE
+			LBItem *item = _vm->getItemById(_words[_currentWord].itemId);
+			if (item)
+				item->togglePlaying(false, true);
+
+			_currentWord = 0xFFFF;
+		}
+	}
+
+	LBItem::update();
+}
+
+void LBLiveTextItem::draw() {
+	// this is only necessary when we are drawing using a bitmap
+	if (!_resourceId)
+		return;
+
+	if (_currentWord != 0xFFFF) {
+		uint yPos = 0;
+		if (_currentWord > 0) {
+			for (uint i = 0; i < _currentWord; i++) {
+				yPos += (_words[i].bounds.bottom - _words[i].bounds.top);
+			}
+		}
+		drawWord(_currentWord, yPos);
+		return;
+	}
+
+	if (_currentPhrase == 0xFFFF)
+		return;
+
+	uint wordStart = _phrases[_currentPhrase].wordStart;
+	uint wordCount = _phrases[_currentPhrase].wordCount;
+	if (wordStart + wordCount > _words.size())
+		error("phrase %d was invalid (%d words, from %d, out of only %d total)",
+			_currentPhrase, wordCount, wordStart, _words.size());
+
+	uint yPos = 0;
+	for (uint i = 0; i < wordStart + wordCount; i++) {
+		if (i >= wordStart)
+			drawWord(i, yPos);
+		yPos += (_words[i].bounds.bottom - _words[i].bounds.top);
+	}
+}
+
+void LBLiveTextItem::drawWord(uint word, uint yPos) {
+	Common::Rect srcRect(0, yPos, _words[word].bounds.right - _words[word].bounds.left,
+		yPos + _words[word].bounds.bottom - _words[word].bounds.top);
+	Common::Rect dstRect = _words[word].bounds;
+	dstRect.translate(_rect.left, _rect.top);
+	_vm->_gfx->copyAnimImageSectionToScreen(_resourceId, srcRect, dstRect);
+}
+
+void LBLiveTextItem::handleMouseDown(Common::Point pos) {
+	if (!_loaded || !_enabled || !_globalEnabled || _playing)
+		return LBItem::handleMouseDown(pos);
+
+	pos.x -= _rect.left;
+	pos.y -= _rect.top;
+
+	for (uint i = 0; i < _words.size(); i++) {
+		if (_words[i].bounds.contains(pos)) {
+			if (_currentWord != 0xFFFF) {
+				paletteUpdate(_currentWord, false);
+				_currentWord = 0xFFFF;
+			}
+			uint16 soundId = _words[i].soundId;
+			if (!soundId) {
+				// TODO: can we be smarter here, using timing?
+				warning("ignoring click due to no soundId");
+				return;
+			}
+			_currentWord = i;
+			_vm->playSound(this, soundId);
+			paletteUpdate(_currentWord, true);
+			return;
+		}
+	}
+
+	return LBItem::handleMouseDown(pos);
+}
+
+bool LBLiveTextItem::togglePlaying(bool playing, bool restart) {
+	if (!playing)
+		return LBItem::togglePlaying(playing, restart);
+	if (!_loaded || !_enabled || !_globalEnabled)
+		return _playing;
+
+	// TODO: handle this properly
+	_vm->_sound->stopSound();
+
+	_currentWord = 0xFFFF;
+	_currentPhrase = 0xFFFF;
+
+	return true;
+}
+
+void LBLiveTextItem::stop() {
+	// TODO: stop sound, refresh palette
+
+	LBItem::stop();
+}
+
+void LBLiveTextItem::notify(uint16 data, uint16 from) {
+	if (!_loaded || !_enabled || !_globalEnabled || !_playing)
+		return LBItem::notify(data, from);
+
+	if (_currentWord != 0xFFFF) {
+		// TODO: handle this properly
+		_vm->_sound->stopSound();
+		paletteUpdate(_currentWord, false);
+		_currentWord = 0xFFFF;
+	}
+
+	for (uint i = 0; i < _phrases.size(); i++) {
+		if (_phrases[i].highlightStart == data && _phrases[i].startId == from) {
+			debug(2, "Enabling phrase %d", i);
+			for (uint j = 0; j < _phrases[i].wordCount; j++) {
+				paletteUpdate(_phrases[i].wordStart + j, true);
+			}
+			_currentPhrase = i;
+			// TODO: not sure this is the correct logic
+			if (i == _phrases.size() - 1) {
+				_currentPhrase = 0xFFFF;
+				done(true);
+			}
+		} else if (_phrases[i].highlightEnd == data && _phrases[i].endId == from) {
+			debug(2, "Disabling phrase %d", i);
+			for (uint j = 0; j < _phrases[i].wordCount; j++) {
+				paletteUpdate(_phrases[i].wordStart + j, false);
+			}
+			_currentPhrase = 0xFFFF;
+		}
+	}
+
+	LBItem::notify(data, from);
+}
+
+LBItem *LBLiveTextItem::createClone() {
+	error("can't clone LBLiveTextItem");
+}
+
+LBPictureItem::LBPictureItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
+	debug(3, "new LBPictureItem");
+}
+
+void LBPictureItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
+	switch (type) {
+	case kLBSetDrawMode:
+		{
+		assert(size == 2);
+		// TODO: this probably sets whether points are always contained (0x10)
+		// or whether the bitmap contents are checked (00, or anything else?)
+		uint16 val = stream->readUint16();
+		debug(2, "LBPictureItem: kLBSetDrawMode: %04x", val);
+		}
+		break;
+
+	default:
+		LBItem::readData(type, size, stream);
+	}
+}
+
+bool LBPictureItem::contains(Common::Point point) {
+	if (!LBItem::contains(point))
+		return false;
+
+	if (!_doHitTest)
+		return true;
+
+	// TODO: only check pixels if necessary
+	return !_vm->_gfx->imageIsTransparentAt(_resourceId, false, point.x - _rect.left, point.y - _rect.top);
+}
+
+void LBPictureItem::init() {
+	_vm->_gfx->preloadImage(_resourceId);
+
+	LBItem::init();
+}
+
+void LBPictureItem::draw() {
+	if (!_loaded || !_visible || !_globalVisible)
+		return;
+
+	_vm->_gfx->copyAnimImageToScreen(_resourceId, _rect.left, _rect.top);
+}
+
+LBItem *LBPictureItem::createClone() {
+	return new LBPictureItem(_vm, _page, _rect);
+}
+
+LBMovieItem::LBMovieItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
+	debug(3, "new LBMovieItem");
+}
+
+LBMovieItem::~LBMovieItem() {
+}
+
+void LBMovieItem::update() {
+	if (_playing) {
+		VideoEntryPtr video = _vm->_video->findVideo(_resourceId);
+		if (!video || video->endOfVideo())
+			done(true);
+	}
+
+	LBItem::update();
+}
+
+bool LBMovieItem::togglePlaying(bool playing, bool restart) {
+	if (playing) {
+		if ((_loaded && _enabled && _globalEnabled) || _phase == kLBPhaseNone) {
+			debug("toggled video for phase %d", _phase);
+			VideoEntryPtr video = _vm->_video->playMovie(_resourceId);
+			if (!video)
+				error("Failed to open tMOV %d", _resourceId);
+
+			video->moveTo(_rect.left, _rect.top);
+			return true;
+		}
+	}
+
+	return LBItem::togglePlaying(playing, restart);
+}
+
+LBItem *LBMovieItem::createClone() {
+	return new LBMovieItem(_vm, _page, _rect);
+}
+
+LBMiniGameItem::LBMiniGameItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
+	debug(3, "new LBMiniGameItem");
+}
+
+LBMiniGameItem::~LBMiniGameItem() {
+}
+
+bool LBMiniGameItem::togglePlaying(bool playing, bool restart) {
+	// HACK: Since we don't support any of these hardcoded mini games yet,
+	// just skip to the most logical page. For optional minigames, this
+	// will return the player to the previous page. For mandatory minigames,
+	// this will send the player to the next page.
+
+	uint16 destPage = 0;
+	bool returnToMenu = false;
+
+	// Figure out what minigame we have and bring us back to a page where
+	// the player can continue
+	if (_desc == "Kitch")     // Green Eggs and Ham: Kitchen minigame
+		destPage = 4;
+	else if (_desc == "Eggs") // Green Eggs and Ham: Eggs minigame
+		destPage = 5;
+	else if (_desc == "Fall") // Green Eggs and Ham: Fall minigame
+		destPage = 13;
+	else if (_desc == "MagicWrite3") // Arthur's Reading Race: "Let Me Write" minigame (Page 3)
+		destPage = 3;
+	else if (_desc == "MagicWrite4") // Arthur's Reading Race: "Let Me Write" minigame (Page 4)
+		destPage = 4;
+	else if (_desc == "MagicSpy5") // Arthur's Reading Race: "I Spy" minigame (Page 5)
+		destPage = 5;
+	else if (_desc == "MagicSpy6") // Arthur's Reading Race: "I Spy" minigame (Page 6)
+		destPage = 6;
+	else if (_desc == "MagicWrite7") // Arthur's Reading Race: "Let Me Write" minigame (Page 7)
+		destPage = 7;
+	else if (_desc == "MagicSpy8") // Arthur's Reading Race: "I Spy" minigame (Page 8)
+		destPage = 8;
+	else if (_desc == "MagicRace") // Arthur's Reading Race: Race minigame
+		returnToMenu = true;
+	else
+		error("Unknown minigame '%s'", _desc.c_str());
+
+	GUI::MessageDialog dialog(Common::String::format("The '%s' minigame is not supported yet.", _desc.c_str()));
+	dialog.runModal();
+
+	// Go back to the menu if requested, otherwise go to the requested page
+	if (returnToMenu)
+		_vm->addNotifyEvent(NotifyEvent(kLBNotifyGoToControls, 1));
+	else
+		_vm->addNotifyEvent(NotifyEvent(kLBNotifyChangePage, destPage));
+
+	return false;
+}
+
+LBItem *LBMiniGameItem::createClone() {
+	error("can't clone LBMiniGameItem");
+}
+
+LBProxyItem::LBProxyItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : LBItem(vm, page, rect) {
+	debug(3, "new LBProxyItem");
+
+	_page = nullptr;
+}
+
+LBProxyItem::~LBProxyItem() {
+	delete _page;
+}
+
+void LBProxyItem::load() {
+	if (_loaded)
+		return;
+
+	Common::String leftover;
+	Common::String filename = _vm->getFileNameFromConfig("Proxies", _desc.c_str(), leftover);
+	if (!leftover.empty())
+		error("LBProxyItem tried loading proxy '%s' but got leftover '%s'", _desc.c_str(), leftover.c_str());
+	uint16 baseId = 0;
+	for (uint i = 0; i < filename.size(); i++) {
+		if (filename[i] == ';') {
+			baseId = atoi(filename.c_str() + i + 1);
+			filename = Common::String(filename.c_str(), i);
+		}
+	}
+
+	debug(1, "LBProxyItem loading archive '%s' with id %d", filename.c_str(), baseId);
+	Archive *pageArchive = _vm->createArchive();
+	if (!tryOpenPage(pageArchive, filename))
+		error("failed to open archive '%s' (for proxy '%s')", filename.c_str(), _desc.c_str());
+	_page = new LBPage(_vm);
+	_page->open(pageArchive, baseId);
+
+	LBItem::load();
+}
+
+void LBProxyItem::unload() {
+	delete _page;
+	_page = nullptr;
+
+	LBItem::unload();
+}
+
+LBItem *LBProxyItem::createClone() {
+	return new LBProxyItem(_vm, _page, _rect);
+}
+
+} // End of namespace Mohawk
diff --git a/engines/mohawk/livingbooks_menus.cpp b/engines/mohawk/livingbooks_menus.cpp
new file mode 100644
index 00000000000..0764364ce1c
--- /dev/null
+++ b/engines/mohawk/livingbooks_menus.cpp
@@ -0,0 +1,307 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mohawk/livingbooks.h"
+
+#include "common/textconsole.h"
+
+namespace Mohawk {
+
+void MohawkEngine_LivingBooks::handleUIMenuClick(uint controlId) {
+	LBItem *item;
+
+	switch (controlId) {
+	case 1:
+		if (getFeatures() & GF_LB_10) {
+			if (!tryLoadPageStart(kLBControlMode, 2))
+				error("couldn't load options page");
+		} else {
+			if (!tryLoadPageStart(kLBControlMode, 3))
+				error("couldn't load options page");
+		}
+		break;
+
+	case 2:
+		item = getItemById(10);
+		if (item)
+			item->destroySelf();
+		item = getItemById(11);
+		if (item)
+			item->destroySelf();
+		item = getItemById(199 + _curLanguage);
+		if (item) {
+			item->setVisible(true);
+			item->togglePlaying(false, true);
+		}
+		break;
+
+	case 3:
+		item = getItemById(10);
+		if (item)
+			item->destroySelf();
+		item = getItemById(11);
+		if (item)
+			item->destroySelf();
+		item = getItemById(12);
+		if (item) {
+			item->setVisible(true);
+			item->togglePlaying(false, true);
+		}
+		break;
+
+	case 4:
+		if (getFeatures() & GF_LB_10) {
+			if (!tryLoadPageStart(kLBControlMode, 3))
+				error("couldn't load quit page");
+		} else {
+			if (!tryLoadPageStart(kLBControlMode, 2))
+				error("couldn't load quit page");
+		}
+		break;
+
+	case 10:
+		item = getItemById(10);
+		if (item)
+			item->destroySelf();
+		item = getItemById(11);
+		if (item) {
+			item->setVisible(true);
+			item->togglePlaying(false);
+		}
+		break;
+
+	case 11:
+		item = getItemById(11);
+		if (item)
+			item->togglePlaying(false, true);
+		break;
+
+	case 12:
+		// start game, in play mode
+		if (!tryLoadPageStart(kLBPlayMode, 1))
+			error("couldn't start play mode");
+		break;
+
+	default:
+		if (controlId >= 100 && controlId < 100 + (uint)_numLanguages) {
+			uint newLanguage = controlId - 99;
+			if (newLanguage == _curLanguage)
+				break;
+			item = getItemById(99 + _curLanguage);
+			if (item)
+				item->seek(1);
+			_curLanguage = newLanguage;
+		} else if (controlId >= 200 && controlId < 200 + (uint)_numLanguages) {
+			// start game, in read mode
+			if (!tryLoadPageStart(kLBReadMode, 1))
+				error("couldn't start read mode");
+		}
+		break;
+	}
+}
+
+void MohawkEngine_LivingBooks::handleUIPoetryMenuClick(uint controlId) {
+	LBItem *item;
+
+	// the menu UI in New Kid on the Block is a hybrid of the normal menu
+	// and the normal options screen
+
+	// TODO: this is mostly untested
+
+	switch (controlId) {
+	case 2:
+	case 3:
+		handleUIOptionsClick(controlId);
+		break;
+
+	case 4:
+		handleUIMenuClick(controlId);
+		break;
+
+	case 6:
+		handleUIMenuClick(2);
+		break;
+
+	case 7:
+		item = getItemById(10);
+		if (item)
+			item->destroySelf();
+		item = getItemById(11);
+		if (item)
+			item->destroySelf();
+		item = getItemById(12);
+		if (item) {
+			item->setVisible(true);
+			item->togglePlaying(false, true);
+		}
+		break;
+
+	case 0xA:
+		item = getItemById(10);
+		if (item)
+			item->destroySelf();
+		item = getItemById(11);
+		if (item) {
+			item->setVisible(true);
+			item->togglePlaying(false);
+		}
+		break;
+
+	case 0xB:
+		item = getItemById(11);
+		if (item)
+			item->togglePlaying(false, true);
+		break;
+
+	case 0xC:
+		if (!tryLoadPageStart(kLBPlayMode, _curSelectedPage))
+			error("failed to load page %d", _curSelectedPage);
+		break;
+
+	default:
+		if (controlId < 100) {
+			handleUIMenuClick(controlId);
+		} else {
+			if (!tryLoadPageStart(kLBReadMode, _curSelectedPage))
+				error("failed to load page %d", _curSelectedPage);
+		}
+	}
+}
+
+void MohawkEngine_LivingBooks::handleUIQuitClick(uint controlId) {
+	LBItem *item;
+
+	switch (controlId) {
+	case 1:
+	case 2:
+		// button clicked, run animation
+		item = getItemById(10);
+		if (item)
+			item->destroySelf();
+		item = getItemById(11);
+		if (item)
+			item->destroySelf();
+		item = getItemById((controlId == 1) ? 12 : 13);
+		if (item) {
+			item->setVisible(true);
+			item->togglePlaying(false);
+		}
+		break;
+
+	case 10:
+	case 11:
+		item = getItemById(11);
+		if (item)
+			item->togglePlaying(false, true);
+		break;
+
+	case 12:
+		// 'yes', I want to quit
+		quitGame();
+		break;
+
+	case 13:
+		// 'no', go back to menu
+		if (!tryLoadPageStart(kLBControlMode, 1))
+			error("couldn't return to menu");
+		break;
+
+	default:
+		break;
+	}
+}
+
+void MohawkEngine_LivingBooks::handleUIOptionsClick(uint controlId) {
+	LBItem *item;
+
+	switch (controlId) {
+	case 1:
+		item = getItemById(10);
+		if (item)
+			item->destroySelf();
+		item = getItemById(202);
+		if (item) {
+			item->setVisible(true);
+			item->togglePlaying(false, true);
+		}
+		break;
+
+	case 2:
+		// back
+		item = getItemById(2);
+		if (item)
+			item->seek(1);
+		if (_curSelectedPage == 1) {
+			_curSelectedPage = _numPages;
+		} else {
+			_curSelectedPage--;
+		}
+		for (uint i = 0; i < _numPages; i++) {
+			item = getItemById(1000 + i);
+			if (item)
+				item->setVisible(_curSelectedPage == i + 1);
+			item = getItemById(1100 + i);
+			if (item)
+				item->setVisible(_curSelectedPage == i + 1);
+		}
+		break;
+
+	case 3:
+		// forward
+		item = getItemById(3);
+		if (item)
+			item->seek(1);
+		if (_curSelectedPage == _numPages) {
+			_curSelectedPage = 1;
+		} else {
+			_curSelectedPage++;
+		}
+		for (uint i = 0; i < _numPages; i++) {
+			item = getItemById(1000 + i);
+			if (item)
+				item->setVisible(_curSelectedPage == i + 1);
+			item = getItemById(1100 + i);
+			if (item)
+				item->setVisible(_curSelectedPage == i + 1);
+		}
+		break;
+
+	case 4:
+		if (!tryLoadPageStart(kLBCreditsMode, 1))
+			error("failed to start credits");
+		break;
+
+	case 5:
+		if (!tryLoadPageStart(kLBPreviewMode, 1))
+			error("failed to start preview");
+		break;
+
+	case 202:
+		if (!tryLoadPageStart(kLBPlayMode, _curSelectedPage))
+			error("failed to load page %d", _curSelectedPage);
+		break;
+
+	default:
+		break;
+	}
+}
+
+} // End of namespace Mohawk
diff --git a/engines/mohawk/livingbooks_page.cpp b/engines/mohawk/livingbooks_page.cpp
new file mode 100644
index 00000000000..775a82cc088
--- /dev/null
+++ b/engines/mohawk/livingbooks_page.cpp
@@ -0,0 +1,153 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mohawk/livingbooks.h"
+#include "mohawk/resource.h"
+
+#include "common/textconsole.h"
+
+namespace Mohawk {
+
+LBPage::LBPage(MohawkEngine_LivingBooks *vm) : _vm(vm) {
+	_code = nullptr;
+	_mhk = nullptr;
+
+	_baseId = 0;
+	_cascade = false;
+}
+
+void LBPage::open(Archive *mhk, uint16 baseId) {
+	_mhk = mhk;
+	_baseId = baseId;
+
+	_vm->addArchive(_mhk);
+	if (!_vm->hasResource(ID_BCOD, baseId)) {
+		// assume that BCOD is mandatory for v4/v5
+		if (_vm->getGameType() == GType_LIVINGBOOKSV4 || _vm->getGameType() == GType_LIVINGBOOKSV5)
+			error("missing BCOD resource (id %d)", baseId);
+		_code = new LBCode(_vm, 0);
+	} else {
+		_code = new LBCode(_vm, baseId);
+	}
+
+	loadBITL(baseId);
+	for (uint i = 0; i < _items.size(); i++)
+		_vm->addItem(_items[i]);
+
+	for (uint32 i = 0; i < _items.size(); i++)
+		_items[i]->init();
+
+	for (uint32 i = 0; i < _items.size(); i++)
+		_items[i]->startPhase(kLBPhaseLoad);
+}
+
+void LBPage::addClonedItem(LBItem *item) {
+	_vm->addItem(item);
+	_items.push_back(item);
+}
+
+void LBPage::itemDestroyed(LBItem *item) {
+	for (uint i = 0; i < _items.size(); i++)
+		if (item == _items[i]) {
+			_items.remove_at(i);
+			return;
+		}
+	error("itemDestroyed didn't find item");
+}
+
+LBPage::~LBPage() {
+	delete _code;
+	_vm->removeItems(_items);
+	for (uint i = 0; i < _items.size(); i++)
+		delete _items[i];
+	_vm->removeArchive(_mhk);
+	delete _mhk;
+}
+
+// Only 1 VSRN resource per page
+uint16 LBPage::getResourceVersion() {
+	Common::SeekableReadStream *versionStream = _vm->getResource(ID_VRSN, _baseId);
+
+	// FIXME: some V2 games have very strange version entries
+	if (versionStream->size() != 2)
+		debug(1, "Version Record size mismatch");
+
+	uint16 version = versionStream->readUint16BE();
+
+	delete versionStream;
+	return version;
+}
+
+void LBPage::loadBITL(uint16 resourceId) {
+	Common::SeekableReadStreamEndian *bitlStream = _vm->wrapStreamEndian(ID_BITL, resourceId);
+
+	while (true) {
+		Common::Rect rect = _vm->readRect(bitlStream);
+		uint16 type = bitlStream->readUint16();
+
+		LBItem *res;
+		switch (type) {
+		case kLBPictureItem:
+			res = new LBPictureItem(_vm, this, rect);
+			break;
+		case kLBAnimationItem:
+			res = new LBAnimationItem(_vm, this, rect);
+			break;
+		case kLBPaletteItem:
+			res = new LBPaletteItem(_vm, this, rect);
+			break;
+		case kLBGroupItem:
+			res = new LBGroupItem(_vm, this, rect);
+			break;
+		case kLBSoundItem:
+			res = new LBSoundItem(_vm, this, rect);
+			break;
+		case kLBLiveTextItem:
+			res = new LBLiveTextItem(_vm, this, rect);
+			break;
+		case kLBMovieItem:
+			res = new LBMovieItem(_vm, this, rect);
+			break;
+		case kLBMiniGameItem:
+			res = new LBMiniGameItem(_vm, this, rect);
+			break;
+		case kLBProxyItem:
+			res = new LBProxyItem(_vm, this, rect);
+			break;
+		default:
+			warning("Unknown item type %04x", type);
+			// fall through
+		case 3: // often used for buttons
+			res = new LBItem(_vm, this, rect);
+			break;
+		}
+
+		res->readFrom(bitlStream);
+		_items.push_back(res);
+
+		if (bitlStream->size() == bitlStream->pos())
+			break;
+	}
+
+	delete bitlStream;
+}
+
+} // End of namespace Mohawk
diff --git a/engines/mohawk/module.mk b/engines/mohawk/module.mk
index edb3923172f..b264987cae7 100644
--- a/engines/mohawk/module.mk
+++ b/engines/mohawk/module.mk
@@ -8,9 +8,14 @@ MODULE_OBJS = \
 	graphics.o \
 	installer_archive.o \
 	livingbooks.o \
+	livingbooks_animation.o \
+	livingbooks_bookinfo.o \
 	livingbooks_code.o \
 	livingbooks_graphics.o \
+	livingbooks_itemtypes.o \
 	livingbooks_lbx.o \
+	livingbooks_menus.o \
+	livingbooks_page.o \
 	metaengine.o \
 	riven_metaengine.o \
 	myst_metaengine.o \


Commit: 7d5666a9f5619e70726468ae2f61f6c8556b4648
    https://github.com/scummvm/scummvm/commit/7d5666a9f5619e70726468ae2f61f6c8556b4648
Author: Alstruit (34786806+Alstruit at users.noreply.github.com)
Date: 2026-06-14T13:00:37+03:00

Commit Message:
MOHAWK: LB - move tryOpenPage function to header

Duplicate function.

Changed paths:
    engines/mohawk/livingbooks.cpp
    engines/mohawk/livingbooks.h


diff --git a/engines/mohawk/livingbooks.cpp b/engines/mohawk/livingbooks.cpp
index fdcd4f1e2bd..9c186ec7eca 100644
--- a/engines/mohawk/livingbooks.cpp
+++ b/engines/mohawk/livingbooks.cpp
@@ -285,7 +285,7 @@ static Common::String replaceColons(const Common::String &in, char replace) {
 }
 
 // Helper function to assist in opening pages
-static bool tryOpenPage(Archive *archive, const Common::String &fileName) {
+bool tryOpenPage(Archive *archive, const Common::String &fileName) {
 	// Try the plain file name first
 	if (archive->openFile(Common::Path(fileName)))
 		return true;
diff --git a/engines/mohawk/livingbooks.h b/engines/mohawk/livingbooks.h
index 7374c7199fd..a9819b3db25 100644
--- a/engines/mohawk/livingbooks.h
+++ b/engines/mohawk/livingbooks.h
@@ -262,6 +262,8 @@ class LBPage;
 class LBGraphics;
 class LBAnimation;
 
+bool tryOpenPage(Archive *archive, const Common::String &fileName);
+
 struct LBScriptEntry {
 	LBScriptEntry();
 	~LBScriptEntry();


Commit: 0d527c60b8029770ffccdae3631efa7eff19b5dc
    https://github.com/scummvm/scummvm/commit/0d527c60b8029770ffccdae3631efa7eff19b5dc
Author: Alstruit (34786806+Alstruit at users.noreply.github.com)
Date: 2026-06-14T13:00:37+03:00

Commit Message:
MOHAWK: LB - Cleanup ~4000 LoC in main LB (II).

Create livingbooks_constants.h to handle lots of kLBs.
Move LBItem respectively. Move LBSCriptEntry to itemscript.
Define header files: item,itemscript,itemtypes,aniation,page.

Changed paths:
  A engines/mohawk/livingbooks_animation.h
  A engines/mohawk/livingbooks_constants.h
  A engines/mohawk/livingbooks_item.cpp
  A engines/mohawk/livingbooks_item.h
  A engines/mohawk/livingbooks_itemscript.cpp
  A engines/mohawk/livingbooks_itemscript.h
  A engines/mohawk/livingbooks_itemtypes.h
  A engines/mohawk/livingbooks_page.h
    engines/mohawk/livingbooks.cpp
    engines/mohawk/livingbooks.h
    engines/mohawk/module.mk


diff --git a/engines/mohawk/livingbooks.cpp b/engines/mohawk/livingbooks.cpp
index 9c186ec7eca..8aa6688eec1 100644
--- a/engines/mohawk/livingbooks.cpp
+++ b/engines/mohawk/livingbooks.cpp
@@ -917,1120 +917,4 @@ void MohawkEngine_LivingBooks::handleNotify(NotifyEvent &event) {
 	}
 }
 
-LBScriptEntry::LBScriptEntry() {
-	state = 0;
-	data = nullptr;
-	argvParam = nullptr;
-	argvTarget = nullptr;
-}
-
-LBScriptEntry::~LBScriptEntry() {
-	delete[] argvParam;
-	delete[] argvTarget;
-	delete[] data;
-
-	for (uint i = 0; i < subentries.size(); i++)
-		delete subentries[i];
-}
-
-LBItem::LBItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : _vm(vm), _page(page), _rect(rect) {
-	if (_vm->getGameType() == GType_LIVINGBOOKSV1 || _vm->getGameType() == GType_LIVINGBOOKSV2)
-		_phase = kLBPhaseInit;
-	else
-		_phase = kLBPhaseLoad;
-
-	_loopMode = 0;
-	_delayMin = 0;
-	_delayMax = 0;
-	_timingMode = kLBAutoNone;
-	_periodMin = 0;
-	_periodMax = 0;
-	_controlMode = kLBControlNone;
-	_soundMode = 0;
-
-	_loaded = false;
-	_enabled = false;
-	_visible = true;
-	_playing = false;
-	_globalEnabled = true;
-	_globalVisible = true;
-	_nextTime = 0;
-	_startTime = 0;
-	_loops = 0;
-
-	_isAmbient = false;
-	_doHitTest = true;
-}
-
-LBItem::~LBItem() {
-	for (uint i = 0; i < _scriptEntries.size(); i++)
-		delete _scriptEntries[i];
-}
-
-void LBItem::readFrom(Common::SeekableReadStreamEndian *stream) {
-	_resourceId = stream->readUint16();
-	_itemId = stream->readUint16();
-	uint16 size = stream->readUint16();
-	_desc = _vm->readString(stream);
-
-	debug(2, "Item: size %d, resource %d, id %d", size, _resourceId, _itemId);
-	debug(2, "Coords: %d, %d, %d, %d", _rect.left, _rect.top, _rect.right, _rect.bottom);
-	debug(2, "String: '%s'", _desc.c_str());
-
-	if (!_itemId)
-		error("Item had invalid item id");
-
-	int endPos = stream->pos() + size;
-	if (endPos > stream->size())
-		error("Item is larger (should end at %d) than stream (size %d)", endPos, (int)stream->size());
-
-	while (true) {
-		if (stream->pos() == endPos)
-			break;
-
-		uint oldPos = stream->pos();
-
-		uint16 dataType = stream->readUint16();
-		uint16 dataSize = stream->readUint16();
-
-		debug(4, "Data type %04x, size %d", dataType, dataSize);
-		byte *buf = new byte[dataSize];
-		stream->read(buf, dataSize);
-		readData(dataType, dataSize, buf);
-		delete[] buf;
-
-		if ((uint)stream->pos() != oldPos + 4 + (uint)dataSize)
-			error("Failed to read correct number of bytes (off by %d) for data type %04x (size %d)",
-				(int)stream->pos() - (int)(oldPos + 4 + (uint)dataSize), dataType, dataSize);
-
-		if (stream->pos() > endPos)
-			error("Read off the end (at %d) of data (ends at %d)", (int)stream->pos(), endPos);
-
-		assert(!stream->eos());
-	}
-}
-
-LBScriptEntry *LBItem::parseScriptEntry(uint16 type, uint16 &size, Common::MemoryReadStreamEndian *stream, bool isSubentry) {
-	if (size < 6)
-		error("Script entry of type 0x%04x was too small (%d)", type, size);
-
-	uint16 expectedEndSize = 0;
-
-	LBScriptEntry *entry = new LBScriptEntry;
-	entry->type = type;
-	if (isSubentry) {
-		expectedEndSize = size - (stream->readUint16() + 2);
-		entry->event = 0xffff;
-	} else
-		entry->event = stream->readUint16();
-	entry->opcode = stream->readUint16();
-	entry->param = stream->readUint16();
-	debug(4, "Script entry: type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x",
-			entry->type, entry->event, entry->opcode, entry->param);
-	size -= 6;
-
-	// TODO: read as bytes, if this is correct (but beware endianism)
-	byte conditionTag = (entry->event & 0xff00) >> 8;
-	entry->event = entry->event & 0xff;
-
-	if (type == kLBMsgListScript && entry->opcode == kLBOpRunSubentries) {
-		debug(4, "%d script subentries:", entry->param);
-		entry->argc = 0;
-		for (uint i = 0; i < entry->param; i++) {
-			LBScriptEntry *subentry = parseScriptEntry(type, size, stream, true);
-			entry->subentries.push_back(subentry);
-
-			// subentries are aligned
-			if (i + 1 < entry->param && size % 2 == 1) {
-				stream->skip(1);
-				size--;
-			}
-		}
-	} else if (type == kLBMsgListScript) {
-		if (size < 2)
-			error("Script entry of type 0x%04x was too small (%d)", type, size);
-
-		entry->argc = stream->readUint16();
-		size -= 2;
-
-		entry->targetingType = 0;
-
-		uint16 targetingType = entry->argc;
-		if (targetingType == kTargetTypeExpression || targetingType == kTargetTypeCode
-			|| targetingType == kTargetTypeName) {
-			entry->targetingType = targetingType;
-
-			// FIXME
-			if (targetingType == kTargetTypeCode)
-				error("encountered kTargetTypeCode");
-
-			if (size < 2)
-				error("not enough bytes (%d) reading special targeting", size);
-			uint16 count = stream->readUint16();
-			size -= 2;
-
-			debug(4, "%d targets with targeting type %04x", count, targetingType);
-
-			uint oldAlign = size % 2;
-			for (uint i = 0; i < count; i++) {
-				Common::String target = _vm->readString(stream);
-				debug(4, "target '%s'", target.c_str());
-				entry->targets.push_back(target);
-				if (target.size() + 1 > size)
-					error("failed to read target (ran out of stream)");
-				size -= target.size() + 1;
-			}
-			entry->argc = entry->targets.size();
-
-			if ((uint)(size % 2) != oldAlign) {
-				stream->skip(1);
-				size--;
-			}
-		} else if (entry->argc) {
-			entry->argvParam = new uint16[entry->argc];
-			entry->argvTarget = new uint16[entry->argc];
-			debug(4, "With %d targets:", entry->argc);
-
-			if (size < (entry->argc * 4))
-				error("Script entry of type 0x%04x was too small (%d)", type, size);
-
-			for (uint i = 0; i < entry->argc; i++) {
-				entry->argvParam[i] = stream->readUint16();
-				entry->argvTarget[i] = stream->readUint16();
-				debug(4, "Target %d, param 0x%04x", entry->argvTarget[i], entry->argvParam[i]);
-			}
-
-			size -= (entry->argc * 4);
-		}
-	}
-
-	if (type == kLBMsgListScript && entry->opcode == kLBOpJumpUnlessExpression) {
-		if (size < 6)
-			error("not enough bytes (%d) in kLBOpJumpUnlessExpression, event 0x%04x", size, entry->event);
-		entry->offset = stream->readUint32();
-		entry->target = stream->readUint16();
-		debug(4, "kLBOpJumpUnlessExpression: offset %08x, target %d", entry->offset, entry->target);
-		size -= 6;
-	}
-	if (type == kLBMsgListScript && entry->opcode == kLBOpJumpToExpression) {
-		if (size < 4)
-			error("not enough bytes (%d) in kLBOpJumpToExpression, event 0x%04x", size, entry->event);
-		entry->offset = stream->readUint32();
-		debug(4, "kLBOpJumpToExpression: offset %08x", entry->offset);
-		size -= 4;
-	}
-
-	if (type == kLBNotifyScript && entry->opcode == kLBNotifyChangeMode && _vm->getGameType() != GType_LIVINGBOOKSV1) {
-		switch (entry->param) {
-		case 1:
-			if (size < 8)
-				error("%d unknown bytes in notify entry kLBNotifyChangeMode", size);
-			entry->newUnknown = stream->readUint16();
-			entry->newMode = stream->readUint16();
-			entry->newPage = stream->readUint16();
-			entry->newSubpage = stream->readUint16();
-			debug(4, "kLBNotifyChangeMode: unknown %04x, mode %d, page %d.%d",
-				entry->newUnknown, entry->newMode, entry->newPage, entry->newSubpage);
-			size -= 8;
-			break;
-		case 3:
-			{
-			Common::String newCursor = _vm->readString(stream);
-			entry->newCursor = newCursor;
-			if (size < newCursor.size() + 1)
-				error("failed to read newCursor in notify entry");
-			size -= newCursor.size() + 1;
-			debug(4, "kLBNotifyChangeMode: new cursor '%s'", newCursor.c_str());
-			}
-			break;
-		default:
-			// the original engine also does something when param==2 (but not a notify)
-			error("unknown v2 kLBNotifyChangeMode type %d", entry->param);
-		}
-	}
-	if (entry->opcode == kLBOpSendExpression) {
-		if (size < 4)
-			error("not enough bytes (%d) in kLBOpSendExpression, event 0x%04x", size, entry->event);
-		entry->offset = stream->readUint32();
-		debug(4, "kLBOpSendExpression: offset %08x", entry->offset);
-		size -= 4;
-	}
-	if (entry->opcode == kLBOpRunData) {
-		if (size < 4)
-			error("didn't get enough bytes (%d) to read data header in script entry", size);
-		entry->dataType = stream->readUint16();
-		entry->dataLen = stream->readUint16();
-		size -= 4;
-
-		if (size < entry->dataLen)
-			error("didn't get enough bytes (%d) to read data in script entry", size);
-
-		if (entry->dataType == kLBCommand) {
-			Common::String command = _vm->readString(stream);
-			uint commandSize = command.size() + 1;
-			if (commandSize > entry->dataLen)
-				error("failed to read command in script entry: dataLen %d, command '%s' (%d chars)",
-					 entry->dataLen, command.c_str(), commandSize);
-			entry->dataLen = commandSize;
-			entry->data = new byte[commandSize];
-			memcpy(entry->data, command.c_str(), commandSize);
-			size -= commandSize;
-		} else {
-			if (conditionTag)
-				error("kLBOpRunData had unexpected conditionTag");
-			entry->data = new byte[entry->dataLen];
-			stream->read(entry->data, entry->dataLen);
-			size -= entry->dataLen;
-		}
-	}
-	if (entry->event == kLBEventNotified) {
-		if (size < 4)
-			error("not enough bytes (%d) in kLBEventNotified, opcode 0x%04x", size, entry->opcode);
-		entry->matchFrom = stream->readUint16();
-		entry->matchNotify = stream->readUint16();
-		debug(4, "kLBEventNotified: matches %04x (from %04x)",
-			entry->matchNotify, entry->matchFrom);
-		size -= 4;
-	}
-
-	if (isSubentry) {
-		// TODO: subentries may be aligned, so this check is a bit too relaxed
-		if (size != expectedEndSize && size != expectedEndSize + 1)
-			error("expected %d bytes left at end of subentry, but had %d",
-				expectedEndSize, size);
-		return entry;
-	}
-
-	if (conditionTag == 1) {
-		if (!size)
-			error("failed to read condition (empty stream)");
-		Common::String condition = _vm->readString(stream);
-		if (condition.size() == 0) {
-			size--;
-			if (!size)
-				error("failed to read condition (null byte, then ran out of stream)");
-			condition = _vm->readString(stream);
-		}
-		if (condition.size() + 1 > size)
-			error("failed to read condition (ran out of stream)");
-		size -= (condition.size() + 1);
-
-		entry->conditions.push_back(condition);
-		debug(4, "script entry condition '%s'", condition.c_str());
-	} else if (conditionTag == 2) {
-		if (size < 4)
-			error("expected more than %d bytes for conditionTag 2", size);
-		// FIXME
-		stream->skip(4);
-		size -= 4;
-	}
-
-	if (size == 1) {
-		// FIXME: this is alignment, but why?
-		stream->skip(1);
-		size--;
-	} else if (size)
-		error("failed to read script entry correctly (%d bytes left): type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x",
-			size, entry->type, entry->event, entry->opcode, entry->param);
-
-	return entry;
-}
-
-void LBItem::readData(uint16 type, uint16 size, byte *data) {
-	Common::MemoryReadStreamEndian stream(data, size, _vm->isBigEndian());
-	readData(type, size, &stream);
-}
-
-void LBItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
-	switch (type) {
-	case kLBMsgListScript:
-	case kLBNotifyScript:
-		_scriptEntries.push_back(parseScriptEntry(type, size, stream));
-		break;
-
-	case kLBSetPlayInfo:
-		{
-		if (size != 20)
-			error("kLBSetPlayInfo had wrong size (%d)", size);
-
-		_loopMode = stream->readUint16();
-		_delayMin = stream->readUint16();
-		_delayMax = stream->readUint16();
-		_timingMode = stream->readUint16();
-		if (_timingMode > 7)
-			error("encountered timing mode %04x", _timingMode);
-		_periodMin = stream->readUint16();
-		_periodMax = stream->readUint16();
-		_relocPoint.x = stream->readSint16();
-		_relocPoint.y = stream->readSint16();
-		_controlMode = stream->readUint16();
-		_soundMode = stream->readUint16();
-
-		debug(2, "kLBSetPlayInfo: loop mode %d (%d to %d), timing mode %d (%d to %d), reloc (%d, %d), control mode %04x, sound mode %04x",
-			_loopMode, _delayMin, _delayMax,
-			_timingMode, _periodMin, _periodMax,
-			_relocPoint.x, _relocPoint.y,
-			_controlMode, _soundMode);
-		}
-		break;
-
-	case kLBSetPlayPhase:
-		if (size != 2)
-			error("SetPlayPhase had wrong size (%d)", size);
-		_phase = stream->readUint16();
-		debug(2, "kLBSetPlayPhase: %d", _phase);
-		break;
-
-	case kLBSetKeyNotify:
-		{
-		// FIXME: variable-size notifies, targets
-		if (size != 18)
-			error("0x6f had wrong size (%d)", size);
-		uint event = stream->readUint16();
-		LBKey key;
-		stream->read(&key, 4);
-		uint opcode = stream->readUint16();
-		uint param = stream->readUint16();
-		uint u6 = stream->readUint16();
-		uint u7 = stream->readUint16();
-		uint u8 = stream->readUint16();
-		uint u9 = stream->readUint16();
-		warning("ignoring kLBSetKeyNotify: item %s, key code %02x (modifier mask %d, char %d, repeat %d), event %04x, opcode %04x, param %04x, unknowns %04x, %04x, %04x, %04x",
-			_desc.c_str(), key.code, key.modifiers, key.char_, key.repeats, event, opcode, param, u6, u7, u8, u9);
-		}
-		break;
-
-	case kLBCommand:
-		{
-			Common::String command = _vm->readString(stream);
-			if (size != command.size() + 1)
-				error("failed to read command string");
-
-			runCommand(command);
-		}
-		break;
-
-	case kLBSetNotVisible:
-		assert(size == 0);
-		_visible = false;
-		break;
-
-	case kLBGlobalDisable:
-		assert(size == 0);
-		_globalEnabled = false;
-		break;
-
-	case kLBGlobalSetNotVisible:
-		assert(size == 0);
-		_globalVisible = false;
-		break;
-
-	case kLBSetAmbient:
-		assert(size == 0);
-		_isAmbient = true;
-		break;
-
-	case kLBSetKeyEvent:
-		{
-		// FIXME: targets
-		if (size != 10)
-			error("kLBSetKeyEvent had wrong size (%d)", size);
-		uint u3 = stream->readUint16();
-		LBKey key;
-		stream->read(&key, 4);
-		uint target = stream->readUint16();
-		uint16 event = stream->readUint16();
-		// FIXME: this is scripting stuff: what to run when key is pressed
-		warning("ignoring kLBSetKeyEvent: item %s, key code %02x (modifier mask %d, char %d, repeat %d) unknown %04x, target %d, event %04x",
-			_desc.c_str(), key.code, key.modifiers, key.char_, key.repeats, u3, target, event);
-		}
-		break;
-
-	case kLBSetHitTest:
-		{
-		assert(size == 2);
-		uint val = stream->readUint16();
-		_doHitTest = (bool)val;
-		debug(2, "kLBSetHitTest (on %s): value %04x", _desc.c_str(), val);
-		}
-		break;
-
-	case kLBSetRolloverData:
-		{
-		assert(size == 2);
-		uint16 flag = stream->readUint16();
-		warning("ignoring kLBSetRolloverData: item %s, flag %d", _desc.c_str(), flag);
-		}
-		break;
-
-	case kLBSetParent:
-		{
-		assert(size == 2);
-		uint16 parent = stream->readUint16();
-		warning("ignoring kLBSetParent: item %s, parent id %d", _desc.c_str(), parent);
-		}
-		break;
-
-	case kLBUnknown194:
-		{
-		assert(size == 4);
-		uint offset = stream->readUint32();
-		_page->_code->runCode(this, offset);
-		}
-		break;
-
-	default:
-		error("Unknown message %04x (size 0x%04x)", type, size);
-		//for (uint i = 0; i < size; i++)
-		//	debugN("%02x ", stream->readByte());
-		//debugN("\n");
-		break;
-	}
-}
-
-void LBItem::destroySelf() {
-	if (!this->_itemId)
-		error("destroySelf() on an item which was already dead");
-
-	_vm->queueDelayedEvent(DelayedEvent(this, kLBDelayedEventDestroy));
-
-	_itemId = 0;
-}
-
-void LBItem::setEnabled(bool enabled) {
-	if (enabled && !_loaded && !_playing) {
-		if (_timingMode == kLBAutoUserIdle) {
-			setNextTime(_periodMin, _periodMax);
-			debug(2, "Enable time startup");
-		}
-	}
-
-	_enabled = enabled;
-}
-
-void LBItem::setGlobalEnabled(bool enabled) {
-	bool wasEnabled = _loaded && _enabled && _globalEnabled;
-	_globalEnabled = enabled;
-	if (wasEnabled != (_loaded && _enabled && _globalEnabled))
-		setEnabled(enabled);
-}
-
-bool LBItem::contains(Common::Point point) {
-	if (!_loaded)
-		return false;
-
-	if (_playing && _loopMode == 0xFFFF)
-		stop();
-
-	if (!_playing && _timingMode == kLBAutoUserIdle)
-		setNextTime(_periodMin, _periodMax);
-
-	return _visible && _globalVisible && _rect.contains(point);
-}
-
-void LBItem::update() {
-	if (_phase != kLBPhaseNone && (!_loaded || !_enabled || !_globalEnabled))
-		return;
-
-	if (_nextTime == 0 || _nextTime > (uint32)(_vm->_system->getMillis() / 16))
-		return;
-
-	if (togglePlaying(_playing, true)) {
-		_nextTime = 0;
-	} else if (_loops == 0 && _timingMode == kLBAutoUserIdle) {
-		debug(9, "Looping in update()");
-		setNextTime(_periodMin, _periodMax);
-	}
-}
-
-void LBItem::handleMouseDown(Common::Point pos) {
-	if (!_loaded || !_enabled || !_globalEnabled)
-		return;
-
-	_vm->setFocus(this);
-	runScript(kLBEventMouseDown);
-	runScript(kLBEventMouseTrackIn);
-}
-
-void LBItem::handleMouseMove(Common::Point pos) {
-	// TODO: handle drag
-}
-
-void LBItem::handleMouseUp(Common::Point pos) {
-	_vm->setFocus(nullptr);
-	runScript(kLBEventMouseUp);
-	runScript(kLBEventMouseUpIn);
-}
-
-bool LBItem::togglePlaying(bool playing, bool restart) {
-	if (playing) {
-		_vm->queueDelayedEvent(DelayedEvent(this, kLBDelayedEventDone));
-		return true;
-	}
-	if (((_loaded && _enabled && _globalEnabled) || _phase == kLBPhaseNone) && !_playing) {
-		_playing = togglePlaying(true, restart);
-		if (_playing) {
-			_nextTime = 0;
-			_startTime = _vm->_system->getMillis() / 16;
-
-			if (_loopMode == 0xFFFF || _loopMode == 0xFFFE)
-				_loops = 0xFFFF;
-			else
-				_loops = _loopMode;
-
-			if (_controlMode >= kLBControlHideMouse) {
-				debug(2, "Hiding cursor");
-				_vm->_cursor->hideCursor();
-				_vm->lockSound(this, true);
-
-				if (_controlMode >= kLBControlPauseItems) {
-					debug(2, "Disabling all");
-					_vm->setEnableForAll(false, this);
-				}
-			}
-
-			runScript(kLBEventStarted);
-			notify(0, _itemId);
-		}
-	}
-	return _playing;
-}
-
-void LBItem::done(bool onlyNotify) {
-	if (onlyNotify) {
-		if (_relocPoint.x || _relocPoint.y) {
-			_rect.translate(_relocPoint.x, _relocPoint.y);
-			// TODO: does drag box need adjusting?
-		}
-
-		if (_loops && --_loops) {
-			debug(9, "Real looping (now 0x%04x left)", _loops);
-			setNextTime(_delayMin, _delayMax, _startTime);
-		} else
-			done(false);
-
-		return;
-	}
-
-	_playing = false;
-	_loops = 0;
-	_startTime = 0;
-
-	if (_controlMode >= kLBControlHideMouse) {
-		debug(2, "Showing cursor");
-		_vm->_cursor->showCursor();
-		_vm->lockSound(this, false);
-
-		if (_controlMode >= kLBControlPauseItems) {
-			debug(2, "Enabling all");
-			_vm->setEnableForAll(true, this);
-		}
-	}
-
-	if (_timingMode == kLBAutoUserIdle) {
-		debug(9, "Looping in done() - %d to %d", _periodMin, _periodMax);
-		setNextTime(_periodMin, _periodMax);
-	}
-
-	runScript(kLBEventDone);
-	notify(0xFFFF, _itemId);
-}
-
-void LBItem::init() {
-	runScript(kLBEventInit);
-}
-
-void LBItem::setVisible(bool visible) {
-	if (visible == _visible)
-		return;
-
-	_visible = visible;
-	_vm->_needsRedraw = true;
-}
-
-void LBItem::setGlobalVisible(bool visible) {
-	bool wasEnabled = _visible && _globalVisible;
-	_globalVisible = visible;
-	if (wasEnabled != (_visible && _globalVisible))
-		_vm->_needsRedraw = true;
-}
-
-void LBItem::startPhase(uint phase) {
-	if (_phase == phase) {
-		if (_phase != kLBPhaseNone) {
-			setEnabled(true);
-		}
-
-		load();
-	}
-
-	switch (phase) {
-	case kLBPhaseLoad:
-		runScript(kLBEventListLoad);
-		break;
-	case kLBPhaseCreate:
-		runScript(kLBEventPhaseCreate);
-		if (_timingMode == kLBAutoCreate) {
-			debug(2, "Phase create: time startup");
-			setNextTime(_periodMin, _periodMax);
-		}
-		break;
-	case kLBPhaseInit:
-		runScript(kLBEventPhaseInit);
-		if (_timingMode == kLBAutoInit) {
-			debug(2, "Phase init: time startup");
-			setNextTime(_periodMin, _periodMax);
-		}
-		break;
-	case kLBPhaseIntro:
-		runScript(kLBEventPhaseIntro);
-		if (_timingMode == kLBAutoIntro || _timingMode == kLBAutoUserIdle) {
-			debug(2, "Phase intro: time startup");
-			setNextTime(_periodMin, _periodMax);
-		}
-		break;
-	case kLBPhaseMain:
-		runScript(kLBEventPhaseMain);
-		if (_timingMode == kLBAutoUserIdle || _timingMode == kLBAutoMain) {
-			debug(2, "Phase main: time startup");
-			setNextTime(_periodMin, _periodMax);
-		}
-		break;
-	default:
-		break;
-	}
-}
-
-void LBItem::stop() {
-	if (!_playing)
-		return;
-
-	_loops = 0;
-	seek(0xFFFF);
-	done(true);
-}
-
-void LBItem::notify(uint16 data, uint16 from) {
-	if (_timingMode == kLBAutoSync) {
-		// TODO: is this correct?
-		if (_periodMin == data && _periodMax == from) {
-			debug(2, "Handling notify 0x%04x (from %d)", data, from);
-			setNextTime(0, 0);
-		}
-	}
-
-	runScript(kLBEventNotified, data, from);
-}
-
-void LBItem::load() {
-	if (_loaded)
-		return;
-
-	_loaded = true;
-
-	// FIXME: events etc
-	if (_timingMode == kLBAutoLoad) {
-		debug(2, "Load: time startup");
-		setNextTime(_periodMin, _periodMax);
-	}
-}
-
-void LBItem::unload() {
-	if (!_loaded)
-		return;
-
-	_loaded = false;
-
-	// FIXME: stuff
-}
-
-void LBItem::moveBy(const Common::Point &pos) {
-	_rect.translate(pos.x, pos.y);
-}
-
-void LBItem::moveTo(const Common::Point &pos) {
-	_rect.moveTo(pos);
-}
-
-LBItem *LBItem::clone(uint16 newId, const Common::String &newName) {
-	LBItem *item = createClone();
-
-	item->_itemId = newId;
-	item->_desc = newName;
-
-	item->_resourceId = _resourceId;
-	// FIXME: the rest
-
-	_page->addClonedItem(item);
-	// FIXME: zorder?
-	return item;
-}
-
-LBItem *LBItem::createClone() {
-	return new LBItem(_vm, _page, _rect);
-}
-
-void LBItem::runScript(uint event, uint16 data, uint16 from) {
-	for (uint i = 0; i < _scriptEntries.size(); i++) {
-		LBScriptEntry *entry = _scriptEntries[i];
-
-		if (entry->event != event)
-			continue;
-
-		if (event == kLBEventNotified) {
-			if ((entry->matchFrom && entry->matchFrom != from) || entry->matchNotify != data)
-				continue;
-		}
-
-		bool conditionsMatch = true;
-		for (uint n = 0; n < entry->conditions.size(); n++) {
-			if (!checkCondition(entry->conditions[n])) {
-				conditionsMatch = false;
-				break;
-			}
-		}
-		if (!conditionsMatch)
-			continue;
-
-		if (entry->type == kLBNotifyScript) {
-			debug(2, "Notify: event 0x%04x, opcode 0x%04x, param 0x%04x",
-				entry->event, entry->opcode, entry->param);
-
-			if (entry->opcode == kLBNotifyGUIAction)
-				_vm->addNotifyEvent(NotifyEvent(entry->opcode, _itemId));
-			else if (entry->opcode == kLBNotifyChangeMode && _vm->getGameType() != GType_LIVINGBOOKSV1) {
-				NotifyEvent notifyEvent(entry->opcode, entry->param);
-				notifyEvent.newUnknown = entry->newUnknown;
-				notifyEvent.newMode = entry->newMode;
-				notifyEvent.newPage = entry->newPage;
-				notifyEvent.newSubpage = entry->newSubpage;
-				notifyEvent.newCursor = entry->newCursor;
-				_vm->addNotifyEvent(notifyEvent);
-			} else
-				_vm->addNotifyEvent(NotifyEvent(entry->opcode, entry->param));
-		} else
-			runScriptEntry(entry);
-	}
-}
-
-int LBItem::runScriptEntry(LBScriptEntry *entry) {
-	if (entry->state == 0xffff)
-		return 0;
-
-	uint start = 0;
-	uint count = entry->argc;
-	// zero targets = apply to self
-	if (!count)
-		count = 1;
-
-	if (entry->opcode != kLBOpRunSubentries) switch (entry->param) {
-	case 0xfffe:
-		// Run once (disable self after run).
-		entry->state = 0xffff;
-		break;
-	case 0xffff:
-		break;
-	case 0:
-	case 1:
-	case 2:
-		start = entry->state;
-		entry->state++;
-		if (entry->state >= count) {
-			switch (entry->param) {
-			case 0:
-				// Disable..
-				entry->state = 0xffff;
-				return 0;
-			case 1:
-				// Stay at the end.
-				entry->state = count - 1;
-				break;
-			case 2:
-				// Loop.
-				entry->state = 0;
-				break;
-			default:
-				break;
-			}
-		}
-		count = 1;
-		break;
-	case 3:
-		// Pick random target.
-		start = _vm->_rnd->getRandomNumberRng(0, count);
-		count = 1;
-		break;
-	default:
-		warning("Weird param for script entry (type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x)",
-			entry->type, entry->event, entry->opcode, entry->param);
-	}
-
-	for (uint n = start; n < count; n++) {
-		LBItem *target;
-
-		debug(2, "Script run: type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x",
-			entry->type, entry->event, entry->opcode, entry->param);
-
-		if (entry->argc) {
-			switch (entry->targetingType) {
-			case kTargetTypeExpression:
-				{
-				// FIXME: this should be EVALUATED
-				LBValue &tgt = _vm->_variables[entry->targets[n]];
-				switch (tgt.type) {
-				case kLBValueItemPtr:
-					target = tgt.item;
-					break;
-				case kLBValueString:
-					// FIXME: handle 'self', at least
-					// TODO: correct otherwise? or only self?
-					target = _vm->getItemByName(tgt.string);
-					break;
-				case kLBValueInteger:
-					target = _vm->getItemById(tgt.integer);
-					break;
-				default:
-					// FIXME: handle list
-					warning("Target '%s' (by expression) resulted in unknown type, skipping", entry->targets[n].c_str());
-					continue;
-				}
-				}
-				if (!target) {
-					debug(2, "Target '%s' (by expression) doesn't exist, skipping", entry->targets[n].c_str());
-					continue;
-				}
-				debug(2, "Target: '%s' (expression '%s')", target->_desc.c_str(), entry->targets[n].c_str());
-				break;
-			case kTargetTypeCode:
-				// FIXME
-				error("encountered kTargetTypeCode");
-				break;
-			case kTargetTypeName:
-				// FIXME: handle 'self'
-				target = _vm->getItemByName(entry->targets[n]);
-				if (!target) {
-					debug(2, "Target '%s' (by name) doesn't exist, skipping", entry->targets[n].c_str());
-					continue;
-				}
-				debug(2, "Target: '%s' (by name)", target->_desc.c_str());
-				break;
-			default:
-				uint16 targetId = entry->argvTarget[n];
-				// TODO: is this type, perhaps?
-				uint16 param = entry->argvParam[n];
-				target = _vm->getItemById(targetId);
-				if (!target) {
-					debug(2, "Target %04x (%04x) doesn't exist, skipping", targetId, param);
-					continue;
-				}
-				debug(2, "Target: %04x (%04x) '%s'", targetId, param, target->_desc.c_str());
-			}
-		} else {
-			target = this;
-			debug(2, "Self-target on '%s'", _desc.c_str());
-		}
-
-		// an opcode in the form 0x1xx means to run the script for event 0xx
-		if ((entry->opcode & 0xff00) == 0x0100) {
-			// FIXME: pass on param
-			target->runScript(entry->opcode & 0xff);
-			break;
-		}
-
-		switch (entry->opcode) {
-		case kLBOpNone:
-			warning("ignoring kLBOpNone (event 0x%04x, param 0x%04x, target '%s')",
-					entry->event, entry->param, target->_desc.c_str());
-			break;
-
-		case kLBOpXShow:
-			// TODO: should be setVisible(true) - not a delayed event -
-			// when we're doing the param 1/2/3 stuff above?
-			// and in modern LB this is perhaps just a direct target->setVisible(true)..
-			if (_vm->getGameType() != GType_LIVINGBOOKSV1)
-				warning("kLBOpXShow on '%s' is probably broken", target->_desc.c_str());
-			_vm->queueDelayedEvent(DelayedEvent(this, kLBDelayedEventSetNotVisible));
-			break;
-
-		case kLBOpTogglePlay:
-			target->togglePlaying(false, true);
-			break;
-
-		case kLBOpSetNotVisible:
-			target->setVisible(false);
-			break;
-
-		case kLBOpSetVisible:
-			target->setVisible(true);
-			break;
-
-		case kLBOpDestroy:
-			target->destroySelf();
-			break;
-
-		case kLBOpRewind:
-			target->seek(1);
-			break;
-
-		case kLBOpStop:
-			target->stop();
-			break;
-
-		case kLBOpDisable:
-			target->setEnabled(false);
-			break;
-
-		case kLBOpEnable:
-			target->setEnabled(true);
-			break;
-
-		case kLBOpGlobalSetNotVisible:
-			target->setGlobalVisible(false);
-			break;
-
-		case kLBOpGlobalSetVisible:
-			target->setGlobalVisible(true);
-			break;
-
-		case kLBOpGlobalDisable:
-			target->setGlobalEnabled(false);
-			break;
-
-		case kLBOpGlobalEnable:
-			target->setGlobalEnabled(true);
-			break;
-
-		case kLBOpSeekToEnd:
-			target->seek(0xFFFF);
-			break;
-
-		case kLBOpMute:
-		case kLBOpUnmute:
-			// FIXME
-			warning("ignoring kLBOpMute/Unmute (event 0x%04x, param 0x%04x, target '%s')",
-				entry->event, entry->param, target->_desc.c_str());
-			break;
-
-		case kLBOpLoad:
-			target->load();
-			break;
-
-		case kLBOpPreload:
-			// FIXME
-			warning("ignoring kLBOpPreload (event 0x%04x, param 0x%04x, target '%s')",
-				entry->event, entry->param, target->_desc.c_str());
-			break;
-
-		case kLBOpUnload:
-			target->unload();
-			break;
-
-		case kLBOpSeekToPrev:
-		case kLBOpSeekToNext:
-			// FIXME
-			warning("ignoring kLBOpSeekToPrev/Next (event 0x%04x, param 0x%04x, target '%s')",
-				entry->event, entry->param, target->_desc.c_str());
-			break;
-
-		case kLBOpDragBegin:
-		case kLBOpDragEnd:
-			// FIXME
-			warning("ignoring kLBOpDragBegin/End (event 0x%04x, param 0x%04x, target '%s')",
-				entry->event, entry->param, target->_desc.c_str());
-			break;
-
-		case kLBOpScriptDisable:
-		case kLBOpScriptEnable:
-			// FIXME
-			warning("ignoring kLBOpScriptDisable/Enable (event 0x%04x, param 0x%04x, target '%s')",
-				entry->event, entry->param, target->_desc.c_str());
-			break;
-
-		case kLBOpUnknown1C:
-			// FIXME
-			warning("ignoring kLBOpUnknown1C (event 0x%04x, param 0x%04x, target '%s')",
-				entry->event, entry->param, target->_desc.c_str());
-			break;
-
-		case kLBOpSendExpression:
-			_page->_code->runCode(this, entry->offset);
-			break;
-
-		case kLBOpRunSubentries:
-			for (uint i = 0; i < entry->subentries.size(); i++) {
-				LBScriptEntry *subentry = entry->subentries[i];
-
-				int e = runScriptEntry(subentry);
-
-				switch (subentry->opcode) {
-				case kLBOpJumpUnlessExpression:
-					debug(2, "JumpUnless got %d (to %d, on %d, of %d)", e, subentry->target, i, entry->subentries.size());
-					if (!e)
-						i = subentry->target - 1;
-					break;
-				case kLBOpBreakExpression:
-					debug(2, "BreakExpression");
-					i = entry->subentries.size();
-					break;
-				case kLBOpJumpToExpression:
-					debug(2, "JumpToExpression got %d (on %d, of %d)", e, i, entry->subentries.size());
-					i = e - 1;
-					break;
-				default:
-					break;
-				}
-			}
-			break;
-
-		case kLBOpRunData:
-			readData(entry->dataType, entry->dataLen, entry->data);
-			break;
-
-		case kLBOpJumpUnlessExpression:
-		case kLBOpBreakExpression:
-		case kLBOpJumpToExpression:
-			{
-			LBValue r = _page->_code->runCode(this, entry->offset);
-			// FIXME
-			return r.integer;
-			}
-
-		default:
-			error("Unknown script opcode (type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x, target '%s')",
-				entry->type, entry->event, entry->opcode, entry->param, target->_desc.c_str());
-		}
-	}
-
-	return 0;
-}
-
-void LBItem::setNextTime(uint16 min, uint16 max) {
-	setNextTime(min, max, _vm->_system->getMillis() / 16);
-}
-
-void LBItem::setNextTime(uint16 min, uint16 max, uint32 start) {
-	_nextTime = start + _vm->_rnd->getRandomNumberRng((uint)min, (uint)max);
-	debug(9, "nextTime is now %d frames away", _nextTime - (uint)(_vm->_system->getMillis() / 16));
-}
-
-void LBItem::runCommand(const Common::String &command) {
-	LBCode tempCode(_vm, 0);
-
-	debug(2, "running command '%s'", command.c_str());
-
-	uint offset = tempCode.parseCode(command);
-	tempCode.runCode(this, offset);
-}
-
-bool LBItem::checkCondition(const Common::String &condition) {
-	LBCode tempCode(_vm, 0);
-
-	debug(3, "checking condition '%s'", condition.c_str());
-
-	uint offset = tempCode.parseCode(condition);
-	LBValue result = tempCode.runCode(this, offset);
-
-	return result.toInt();
-}
-
 } // End of namespace Mohawk
diff --git a/engines/mohawk/livingbooks.h b/engines/mohawk/livingbooks.h
index a9819b3db25..6ba5f90572c 100644
--- a/engines/mohawk/livingbooks.h
+++ b/engines/mohawk/livingbooks.h
@@ -25,6 +25,7 @@
 #include "mohawk/mohawk.h"
 #include "mohawk/console.h"
 #include "mohawk/livingbooks_graphics.h"
+#include "mohawk/livingbooks_constants.h"
 #include "mohawk/sound.h"
 #include "mohawk/video.h"
 
@@ -35,6 +36,12 @@
 
 #include "livingbooks_code.h"
 
+#include "mohawk/livingbooks_itemscript.h"
+#include "mohawk/livingbooks_item.h"
+#include "mohawk/livingbooks_itemtypes.h"
+#include "mohawk/livingbooks_animation.h"
+#include "mohawk/livingbooks_page.h"
+
 namespace Common {
 	class SeekableReadStreamEndian;
 	class MemoryReadStreamEndian;
@@ -42,221 +49,6 @@ namespace Common {
 
 namespace Mohawk {
 
-// Engine Debug Flags
-enum {
-	kDebugCode = 1,
-};
-
-#define LBKEY_MOD_CTRL 1
-#define LBKEY_MOD_ALT 2
-#define LBKEY_MOD_SHIFT 4
-
-struct LBKey {
-	byte code;
-	byte modifiers;
-	byte char_;
-	byte repeats;
-};
-
-enum NodeState {
-	kLBNodeDone = 0,
-	kLBNodeRunning = 1,
-	kLBNodeWaiting = 2
-};
-
-enum LBMode {
-	kLBIntroMode = 1,
-	kLBControlMode = 2,
-	kLBCreditsMode = 3,
-	kLBPreviewMode = 4,
-	kLBReadMode = 5,
-	kLBPlayMode = 6
-};
-
-enum {
-	kLBPhaseInit = 0x0,
-	kLBPhaseIntro = 0x1,
-	kLBPhaseMain = 0x2,
-	kLBPhaseNone = 0x7fff,
-	kLBPhaseLoad = 0xfffe,
-	kLBPhaseCreate = 0xffff
-};
-
-// automatic modes used in _timingMode
-enum {
-	kLBAutoNone = 0,
-	kLBAutoIntro = 1,
-	kLBAutoUserIdle = 2,
-	kLBAutoMain = 3,
-	kLBAutoSync = 4,
-	kLBAutoInit = 5,
-	kLBAutoCreate = 6,
-	kLBAutoLoad = 7
-};
-
-// control modes used in _controlMode
-enum {
-	kLBControlNone = 0,
-	kLBControlHideMouse = 1,
-	kLBControlPauseItems = 2
-};
-
-enum {
-	kLBStaticTextItem = 0x1,
-	kLBPictureItem = 0x2,
-	kLBEditTextItem = 0x14,
-	kLBLiveTextItem = 0x15,
-	kLBAnimationItem = 0x40,
-	kLBSoundItem = 0x41,
-	kLBGroupItem = 0x42,
-	kLBMovieItem = 0x43,
-	kLBPaletteAItem = 0x44, // unused?
-	kLBPaletteItem = 0x45,
-	kLBProxyItem = 0x46,
-	kLBMiniGameItem = 666, // EVIL!!!!
-	kLBXDataFileItem = 0x3e9,
-	kLBDiscDectectorItem = 0xfa1
-};
-
-enum {
-	// no 0x1?
-	kLBAnimOpNotify = 0x2,
-	kLBAnimOpSetTempo = 0x3,
-	// no 0x4?
-	kLBAnimOpMoveTo = 0x5,
-	kLBAnimOpWait = 0x6,
-	kLBAnimOpSetCel = 0x7,
-	kLBAnimOpSleepUntil = 0x8,
-	kLBAnimOpDrawMode = 0x9,
-	kLBAnimOpPlaySound = 0xa,
-	kLBAnimOpWaitForSound = 0xb,
-	kLBAnimOpReleaseSound = 0xc,
-	kLBAnimOpResetSound = 0xd,
-	kLBAnimOpSetTempoDiv = 0xe,
-	kLBAnimOpDelay = 0xf
-};
-
-enum {
-	kLBEventPhaseInit = 0,
-	kLBEventPhaseIntro = 1,
-	kLBEventMouseDown = 2,
-	kLBEventStarted = 3,
-	kLBEventDone = 4,
-	kLBEventMouseUp = 5,
-	kLBEventPhaseMain = 6,
-	kLBEventNotified = 7,
-	kLBEventDragStart = 8,
-	kLBEventDragMove = 9,
-	kLBEventDragEnd = 0xa,
-	kLBEventRolloverBegin = 0xb,
-	kLBEventRolloverMove = 0xc,
-	kLBEventRolloverEnd = 0xd,
-	kLBEventMouseUpIn = 0xe,
-	kLBEventMouseUpOut = 0xf,
-	kLBEventMouseTrackIn = 0x10,
-	kLBEventMouseTrackMove = 0x11,
-	kLBEventMouseTrackMoveIn = 0x12,
-	kLBEventMouseTrackMoveOut = 0x13,
-	kLBEventMouseTrackOut = 0x14,
-	kLBEventFocusBegin = 0x15,
-	kLBEventFocusEnd = 0x16,
-	kLBEventInit = 0x17,
-	kLBEventLoad = 0x1a,
-	kLBEventListLoad = 0x1b,
-	kLBEventPhaseCreate = 0xff
-};
-
-enum {
-	kLBGroupData = 0x64,
-	kLBLiveTextData = 0x65,
-	kLBMsgListScript = 0x66,
-	kLBNotifyScript = 0x67,
-	kLBSetPlayInfo = 0x68,
-	kLBSetRandomLoc = 0x69,  // unused?
-	kLBSetDrag = 0x6a,       // unused?
-	kLBSetDrawMode = 0x6b,
-	kLBSetFont = 0x6c,       // unused?
-	kLBSetOneShot = 0x6d,    // unused?
-	kLBSetPlayPhase = 0x6e,
-	// from here, 2.x+
-	kLBSetKeyNotify = 0x6f,
-	kLBCommand = 0x70,
-	kLBPaletteAData = 0x71,  // unused?
-	kLBPaletteXData = 0x72,
-	kLBDisable = 0x73,       // unused?
-	kLBEnable = 0x74,        // unused?
-	kLBSetNotVisible = 0x75,
-	kLBSetVisible = 0x76,    // unused?
-	kLBGlobalDisable = 0x77,
-	kLBGlobalEnable = 0x78,  // unused?
-	kLBGlobalSetNotVisible = 0x79,
-	kLBGlobalSetVisible = 0x7a, // unused?
-	kLBSetAmbient = 0x7b,
-	kLBSetDragParams = 0x7c,
-	kLBSetKeyEvent = 0x7d,
-	kLBSetRolloverData = 0x7e,
-	kLBSetParent = 0x7f,
-	kLBSetHitTest = 0x80,
-	// from here, rugrats
-	kLBUnknown194 = 0x194
-};
-
-enum {
-	kLBOpNone = 0x0,
-	kLBOpXShow = 0x1,
-	kLBOpTogglePlay = 0x2,
-	kLBOpSetNotVisible = 0x3,
-	kLBOpSetVisible = 0x4,
-	kLBOpDestroy = 0x5,
-	kLBOpRewind = 0x6,
-	kLBOpStop = 0x7,
-	kLBOpDisable = 0x8,
-	kLBOpEnable = 0x9,
-	// (no 0xa)
-	kLBOpGlobalSetNotVisible = 0xb,
-	kLBOpGlobalSetVisible = 0xc,
-	kLBOpGlobalDisable = 0xd,
-	kLBOpGlobalEnable = 0xe,
-	kLBOpSeekToEnd = 0xf,
-	// (no 0x10)
-	kLBOpMute = 0x11,
-	kLBOpUnmute = 0x12,
-	kLBOpLoad = 0x13,
-	kLBOpPreload = 0x14,
-	kLBOpUnload = 0x15,
-	kLBOpSeekToNext = 0x16,
-	kLBOpSeekToPrev = 0x17,
-	kLBOpDragBegin = 0x18,
-	kLBOpDragEnd = 0x19,
-	kLBOpScriptDisable = 0x1a,
-	kLBOpScriptEnable = 0x1b,
-	kLBOpUnknown1C = 0x1c,
-	kLBOpSendExpression = 0x1d,
-	kLBOpJumpUnlessExpression = 0xfffb,
-	kLBOpBreakExpression = 0xfffc,
-	kLBOpJumpToExpression = 0xfffd,
-	kLBOpRunSubentries = 0xfffe,
-	kLBOpRunData = 0xffff
-};
-
-enum {
-	kLBNotifyGUIAction = 1,
-	kLBNotifyGoToControls = 2,
-	kLBNotifyChangePage = 3,
-	kLBNotifyGotoQuit = 4,
-	kLBNotifyIntroDone = 5,
-	kLBNotifyChangeMode = 6,
-	kLBNotifyCursorChange = 7,
-	kLBNotifyPrintPage = 0xc,
-	kLBNotifyQuit = 0xd
-};
-
-enum {
-	kTargetTypeExpression = 0x3f3f,
-	kTargetTypeCode = 0xfffe,
-	kTargetTypeName = 0xffff
-};
-
 class MohawkEngine_LivingBooks;
 class LBPage;
 class LBGraphics;
@@ -264,403 +56,6 @@ class LBAnimation;
 
 bool tryOpenPage(Archive *archive, const Common::String &fileName);
 
-struct LBScriptEntry {
-	LBScriptEntry();
-	~LBScriptEntry();
-
-	uint16 state;
-
-	uint16 type;
-	uint16 event;
-	uint16 opcode;
-	uint16 param;
-
-	uint16 argc;
-	uint16 *argvParam;
-	uint16 *argvTarget;
-
-	uint16 targetingType;
-	Common::Array<Common::String> targets;
-
-	// kLBNotifyChangeMode
-	uint16 newUnknown;
-	uint16 newMode;
-	uint16 newPage;
-	uint16 newSubpage;
-	Common::String newCursor;
-
-	// kLBEventNotified
-	uint16 matchFrom;
-	uint16 matchNotify;
-
-	// kLBOpSendExpression
-	uint32 offset;
-	// kLBOpJumpUnlessExpression
-	uint16 target;
-
-	uint16 dataType;
-	uint16 dataLen;
-	byte *data;
-
-	Common::Array<Common::String> conditions;
-	Common::Array<LBScriptEntry *> subentries;
-};
-
-struct LBAnimScriptEntry {
-	byte opcode;
-	byte size;
-	byte *data;
-};
-
-class LBAnimationNode {
-public:
-	LBAnimationNode(MohawkEngine_LivingBooks *vm, LBAnimation *parent, uint16 scriptResourceId);
-	~LBAnimationNode();
-
-	void draw(const Common::Rect &_bounds);
-	void reset();
-	NodeState update(bool seeking = false);
-	bool transparentAt(int x, int y);
-
-protected:
-	MohawkEngine_LivingBooks *_vm;
-	LBAnimation *_parent;
-
-	void loadScript(uint16 resourceId);
-	uint _currentEntry;
-	Common::Array<LBAnimScriptEntry> _scriptEntries;
-
-	uint _currentCel;
-	int16 _xPos, _yPos;
-	uint32 _delay;
-};
-
-class LBAnimationItem;
-
-class LBAnimation {
-public:
-	LBAnimation(MohawkEngine_LivingBooks *vm, LBAnimationItem *parent, uint16 resourceId);
-	~LBAnimation();
-
-	void draw();
-	bool update();
-
-	void start();
-	void seek(uint16 pos);
-	void seekToTime(uint32 time);
-	void stop();
-
-	void playSound(uint16 resourceId);
-	bool soundPlaying(uint16 resourceId, const Common::String &cue);
-
-	bool transparentAt(int x, int y);
-
-	void setTempo(uint16 tempo);
-
-	uint getNumResources() { return _shapeResources.size(); }
-	uint16 getResource(uint num) { return _shapeResources[num]; }
-	Common::Point getOffset(uint num) { return _shapeOffsets[num]; }
-
-	uint32 getCurrentFrame() { return _currentFrame; }
-
-	uint16 getParentId();
-
-protected:
-	MohawkEngine_LivingBooks *_vm;
-	LBAnimationItem *_parent;
-
-	Common::Rect _bounds, _clip;
-	Common::Array<LBAnimationNode *> _nodes;
-
-	uint16 _tempo;
-
-	uint16 _currentSound;
-	CueList _cueList;
-
-	uint32 _lastTime, _currentFrame;
-	bool _running;
-
-	void loadShape(uint16 resourceId);
-	Common::Array<uint16> _shapeResources;
-	Common::Array<Common::Point> _shapeOffsets;
-};
-
-class LBItem {
-	friend class LBCode;
-
-public:
-	LBItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect);
-	virtual ~LBItem();
-
-	void readFrom(Common::SeekableReadStreamEndian *stream);
-	void readData(uint16 type, uint16 size, byte *data);
-	virtual void readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream);
-
-	virtual void destroySelf(); // 0x2
-	virtual void setEnabled(bool enabled); // 0x3
-	virtual void setGlobalEnabled(bool enabled);
-	virtual bool contains(Common::Point point); // 0x7
-	virtual void update(); // 0x8
-	virtual void draw() { } // 0x9
-	virtual void handleKeyChar(Common::Point pos) { } // 0xA
-	virtual void handleMouseDown(Common::Point pos); // 0xB
-	virtual void handleMouseMove(Common::Point pos); // 0xC
-	virtual void handleMouseUp(Common::Point pos); // 0xD
-	virtual bool togglePlaying(bool playing, bool restart = false); // 0xF
-	virtual void done(bool onlyNotify); // 0x10
-	virtual void init(); // 0x11
-	virtual void seek(uint16 pos) { } // 0x13
-	virtual void seekToTime(uint32 time) { }
-	virtual void setFocused(bool focused) { } // 0x14
-	virtual void setVisible(bool visible); // 0x17
-	virtual void setGlobalVisible(bool enabled);
-	virtual void startPhase(uint phase); // 0x18
-	virtual void stop(); // 0x19
-	virtual void notify(uint16 data, uint16 from); // 0x1A
-	virtual void load();
-	virtual void unload();
-	virtual void moveBy(const Common::Point &pos);
-	virtual void moveTo(const Common::Point &pos);
-
-	LBItem *clone(uint16 newId, const Common::String &newName);
-
-	uint16 getId() { return _itemId; }
-	const Common::String &getName() { return _desc; }
-	const Common::Rect &getRect() { return _rect; }
-	uint16 getSoundPriority() { return _soundMode; }
-	bool isLoaded() { return _loaded; }
-	bool isAmbient() { return _isAmbient; }
-
-	Common::List<LBItem *>::iterator _iterator;
-
-	// TODO: make private
-	Common::HashMap<Common::String, LBValue, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> _variables;
-
-protected:
-	MohawkEngine_LivingBooks *_vm;
-	LBPage *_page;
-
-	void setNextTime(uint16 min, uint16 max);
-	void setNextTime(uint16 min, uint16 max, uint32 start);
-
-	Common::Rect _rect;
-	Common::String _desc;
-	uint16 _resourceId;
-	uint16 _itemId;
-
-	bool _loaded, _visible, _globalVisible, _playing, _enabled, _globalEnabled;
-
-	uint32 _nextTime, _startTime;
-	uint16 _loops;
-
-	uint16 _phase, _timingMode, _delayMin, _delayMax;
-	uint16 _loopMode, _periodMin, _periodMax;
-	uint16 _controlMode, _soundMode;
-	Common::Point _relocPoint;
-
-	bool _isAmbient;
-	bool _doHitTest;
-
-	virtual LBItem *createClone();
-
-	Common::Array<LBScriptEntry *> _scriptEntries;
-	void runScript(uint event, uint16 data = 0, uint16 from = 0);
-	int runScriptEntry(LBScriptEntry *entry);
-
-	void runCommand(const Common::String &command);
-	bool checkCondition(const Common::String &condition);
-
-	LBScriptEntry *parseScriptEntry(uint16 type, uint16 &size, Common::MemoryReadStreamEndian *stream, bool isSubentry = false);
-};
-
-class LBSoundItem : public LBItem {
-public:
-	LBSoundItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
-	~LBSoundItem() override;
-
-	void update() override;
-	bool togglePlaying(bool playing, bool restart) override;
-	void stop() override;
-
-protected:
-	LBItem *createClone() override;
-
-	bool _running;
-};
-
-struct GroupEntry {
-	uint entryId;
-	uint entryType;
-};
-
-class LBGroupItem : public LBItem {
-public:
-	LBGroupItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
-
-	void readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) override;
-
-	void destroySelf() override;
-	void setEnabled(bool enabled) override;
-	void setGlobalEnabled(bool enabled) override;
-	bool contains(Common::Point point) override;
-	bool togglePlaying(bool playing, bool restart) override;
-	// 0x12
-	void seek(uint16 pos) override;
-	void setVisible(bool visible) override;
-	void setGlobalVisible(bool visible) override;
-	void startPhase(uint phase) override;
-	void stop() override;
-	void load() override;
-	void unload() override;
-	void moveBy(const Common::Point &pos) override;
-	void moveTo(const Common::Point &pos) override;
-
-protected:
-	LBItem *createClone() override;
-
-	bool _starting;
-
-	Common::Array<GroupEntry> _groupEntries;
-};
-
-class LBPaletteItem : public LBItem {
-public:
-	LBPaletteItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
-	~LBPaletteItem() override;
-
-	void readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) override;
-
-	bool togglePlaying(bool playing, bool restart) override;
-	void update() override;
-
-protected:
-	LBItem *createClone() override;
-
-	uint16 _fadeInPeriod, _fadeInStep, _drawStart, _drawCount;
-	uint32 _fadeInStart, _fadeInCurrent;
-	byte *_palette;
-};
-
-struct LiveTextWord {
-	Common::Rect bounds;
-	uint16 soundId;
-
-	uint16 itemType;
-	uint16 itemId;
-};
-
-struct LiveTextPhrase {
-	uint16 wordStart, wordCount;
-	uint16 highlightStart, highlightEnd;
-	uint16 startId, endId;
-};
-
-class LBLiveTextItem : public LBItem {
-public:
-	LBLiveTextItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
-
-	void readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) override;
-
-	bool contains(Common::Point point) override;
-	void update() override;
-	void draw() override;
-	void handleMouseDown(Common::Point pos) override;
-	bool togglePlaying(bool playing, bool restart) override;
-	void stop() override;
-	void notify(uint16 data, uint16 from) override;
-
-protected:
-	LBItem *createClone() override;
-
-	void paletteUpdate(uint16 word, bool on);
-	void drawWord(uint word, uint yPos);
-
-	uint16 _currentPhrase, _currentWord;
-
-	byte _backgroundColor[4];
-	byte _foregroundColor[4];
-	byte _highlightColor[4];
-	uint16 _paletteIndex;
-
-	Common::Array<LiveTextWord> _words;
-	Common::Array<LiveTextPhrase> _phrases;
-};
-
-class LBPictureItem : public LBItem {
-public:
-	LBPictureItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
-
-	void readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) override;
-
-	bool contains(Common::Point point) override;
-	void draw() override;
-	void init() override;
-
-protected:
-	LBItem *createClone() override;
-};
-
-class LBAnimationItem : public LBItem {
-public:
-	LBAnimationItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
-	~LBAnimationItem() override;
-
-	void setEnabled(bool enabled) override;
-	bool contains(Common::Point point) override;
-	void update() override;
-	void draw() override;
-	bool togglePlaying(bool playing, bool restart) override;
-	void done(bool onlyNotify) override;
-	void init() override;
-	void seek(uint16 pos) override;
-	void seekToTime(uint32 time) override;
-	void startPhase(uint phase) override;
-	void stop() override;
-
-protected:
-	LBItem *createClone() override;
-
-	LBAnimation *_anim;
-	bool _running;
-};
-
-class LBMovieItem : public LBItem {
-public:
-	LBMovieItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
-	~LBMovieItem() override;
-
-	void update() override;
-	bool togglePlaying(bool playing, bool restart) override;
-
-protected:
-	LBItem *createClone() override;
-};
-
-class LBMiniGameItem : public LBItem {
-public:
-	LBMiniGameItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
-	~LBMiniGameItem() override;
-
-	bool togglePlaying(bool playing, bool restart) override;
-
-protected:
-	LBItem *createClone() override;
-};
-
-class LBProxyItem : public LBItem {
-public:
-	LBProxyItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
-	~LBProxyItem() override;
-
-	void load() override;
-	void unload() override;
-
-protected:
-	LBItem *createClone() override;
-
-	class LBPage *_page;
-};
-
 struct NotifyEvent {
 	NotifyEvent(uint t, uint p) : type(t), param(p), newUnknown(0), newMode(0), newPage(0), newSubpage(0) { }
 	uint type;
@@ -686,31 +81,6 @@ struct DelayedEvent {
 	DelayedEventType type;
 };
 
-class LBPage {
-public:
-	LBPage(MohawkEngine_LivingBooks *vm);
-	~LBPage();
-
-	void open(Archive *mhk, uint16 baseId);
-	uint16 getResourceVersion();
-
-	void addClonedItem(LBItem *item);
-	void itemDestroyed(LBItem *item);
-
-	LBCode *_code;
-
-protected:
-	MohawkEngine_LivingBooks *_vm;
-
-	Archive *_mhk;
-	Common::Array<LBItem *> _items;
-
-	uint16 _baseId;
-	bool _cascade;
-
-	void loadBITL(uint16 resourceId);
-};
-
 class MohawkEngine_LivingBooks : public MohawkEngine {
 protected:
 	Common::Error run() override;
diff --git a/engines/mohawk/livingbooks_animation.h b/engines/mohawk/livingbooks_animation.h
new file mode 100644
index 00000000000..73aca01952e
--- /dev/null
+++ b/engines/mohawk/livingbooks_animation.h
@@ -0,0 +1,143 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MOHAWK_LIVINGBOOKS_ANIMATION_H
+#define MOHAWK_LIVINGBOOKS_ANIMATION_H
+
+#include "mohawk/livingbooks_constants.h"
+#include "mohawk/livingbooks_item.h"
+#include "mohawk/sound.h"
+
+#include "common/array.h"
+#include "common/rect.h"
+
+namespace Mohawk {
+
+class MohawkEngine_LivingBooks;
+class LBPage;
+class LBAnimation;
+
+struct LBAnimScriptEntry {
+	byte opcode;
+	byte size;
+	byte *data;
+};
+
+class LBAnimationNode {
+public:
+	LBAnimationNode(MohawkEngine_LivingBooks *vm, LBAnimation *parent, uint16 scriptResourceId);
+	~LBAnimationNode();
+
+	void draw(const Common::Rect &_bounds);
+	void reset();
+	NodeState update(bool seeking = false);
+	bool transparentAt(int x, int y);
+
+protected:
+	MohawkEngine_LivingBooks *_vm;
+	LBAnimation *_parent;
+
+	void loadScript(uint16 resourceId);
+	uint _currentEntry;
+	Common::Array<LBAnimScriptEntry> _scriptEntries;
+
+	uint _currentCel;
+	int16 _xPos, _yPos;
+	uint32 _delay;
+};
+
+class LBAnimationItem;
+
+class LBAnimation {
+public:
+	LBAnimation(MohawkEngine_LivingBooks *vm, LBAnimationItem *parent, uint16 resourceId);
+	~LBAnimation();
+
+	void draw();
+	bool update();
+
+	void start();
+	void seek(uint16 pos);
+	void seekToTime(uint32 time);
+	void stop();
+
+	void playSound(uint16 resourceId);
+	bool soundPlaying(uint16 resourceId, const Common::String &cue);
+
+	bool transparentAt(int x, int y);
+
+	void setTempo(uint16 tempo);
+
+	uint getNumResources() { return _shapeResources.size(); }
+	uint16 getResource(uint num) { return _shapeResources[num]; }
+	Common::Point getOffset(uint num) { return _shapeOffsets[num]; }
+
+	uint32 getCurrentFrame() { return _currentFrame; }
+
+	uint16 getParentId();
+
+protected:
+	MohawkEngine_LivingBooks *_vm;
+	LBAnimationItem *_parent;
+
+	Common::Rect _bounds, _clip;
+	Common::Array<LBAnimationNode *> _nodes;
+
+	uint16 _tempo;
+
+	uint16 _currentSound;
+	CueList _cueList;
+
+	uint32 _lastTime, _currentFrame;
+	bool _running;
+
+	void loadShape(uint16 resourceId);
+	Common::Array<uint16> _shapeResources;
+	Common::Array<Common::Point> _shapeOffsets;
+};
+
+class LBAnimationItem : public LBItem {
+public:
+	LBAnimationItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
+	~LBAnimationItem() override;
+
+	void setEnabled(bool enabled) override;
+	bool contains(Common::Point point) override;
+	void update() override;
+	void draw() override;
+	bool togglePlaying(bool playing, bool restart) override;
+	void done(bool onlyNotify) override;
+	void init() override;
+	void seek(uint16 pos) override;
+	void seekToTime(uint32 time) override;
+	void startPhase(uint phase) override;
+	void stop() override;
+
+protected:
+	LBItem *createClone() override;
+
+	LBAnimation *_anim;
+	bool _running;
+};
+
+} // End of namespace Mohawk
+
+#endif
diff --git a/engines/mohawk/livingbooks_constants.h b/engines/mohawk/livingbooks_constants.h
new file mode 100644
index 00000000000..82a6e25f331
--- /dev/null
+++ b/engines/mohawk/livingbooks_constants.h
@@ -0,0 +1,242 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MOHAWK_LIVINGBOOKS_CONSTANTS_H
+#define MOHAWK_LIVINGBOOKS_CONSTANTS_H
+
+namespace Mohawk {
+
+/* Some disambiguations:
+ *  _soundMode is a priority (see LBItem::getSoundPriority), not a kLBControlMode.
+ *  _controlMode (item flags) is unrelated to kLBControlMode (the menu screens).
+ *  LBProxyItem::_page shadows LBItem::_page (preexisting, intentional-looking, untouched).
+ */
+
+// Engine Debug Flags
+enum {
+	kDebugCode = 1,
+};
+
+#define LBKEY_MOD_CTRL 1
+#define LBKEY_MOD_ALT 2
+#define LBKEY_MOD_SHIFT 4
+enum NodeState {
+	kLBNodeDone = 0,
+	kLBNodeRunning = 1,
+	kLBNodeWaiting = 2
+};
+
+enum LBMode {
+	kLBIntroMode = 1,
+	kLBControlMode = 2,
+	kLBCreditsMode = 3,
+	kLBPreviewMode = 4,
+	kLBReadMode = 5,
+	kLBPlayMode = 6
+};
+
+enum {
+	kLBPhaseInit = 0x0,
+	kLBPhaseIntro = 0x1,
+	kLBPhaseMain = 0x2,
+	kLBPhaseNone = 0x7fff,
+	kLBPhaseLoad = 0xfffe,
+	kLBPhaseCreate = 0xffff
+};
+
+// automatic modes used in _timingMode
+enum {
+	kLBAutoNone = 0,
+	kLBAutoIntro = 1,
+	kLBAutoUserIdle = 2,
+	kLBAutoMain = 3,
+	kLBAutoSync = 4,
+	kLBAutoInit = 5,
+	kLBAutoCreate = 6,
+	kLBAutoLoad = 7
+};
+
+// control modes used in _controlMode
+enum {
+	kLBControlNone = 0,
+	kLBControlHideMouse = 1,
+	kLBControlPauseItems = 2
+};
+
+enum {
+	kLBStaticTextItem = 0x1,
+	kLBPictureItem = 0x2,
+	kLBEditTextItem = 0x14,
+	kLBLiveTextItem = 0x15,
+	kLBAnimationItem = 0x40,
+	kLBSoundItem = 0x41,
+	kLBGroupItem = 0x42,
+	kLBMovieItem = 0x43,
+	kLBPaletteAItem = 0x44, // unused?
+	kLBPaletteItem = 0x45,
+	kLBProxyItem = 0x46,
+	kLBMiniGameItem = 666, // EVIL!!!!
+	kLBXDataFileItem = 0x3e9,
+	kLBDiscDectectorItem = 0xfa1
+};
+
+enum {
+	// no 0x1?
+	kLBAnimOpNotify = 0x2,
+	kLBAnimOpSetTempo = 0x3,
+	// no 0x4?
+	kLBAnimOpMoveTo = 0x5,
+	kLBAnimOpWait = 0x6,
+	kLBAnimOpSetCel = 0x7,
+	kLBAnimOpSleepUntil = 0x8,
+	kLBAnimOpDrawMode = 0x9,
+	kLBAnimOpPlaySound = 0xa,
+	kLBAnimOpWaitForSound = 0xb,
+	kLBAnimOpReleaseSound = 0xc,
+	kLBAnimOpResetSound = 0xd,
+	kLBAnimOpSetTempoDiv = 0xe,
+	kLBAnimOpDelay = 0xf
+};
+
+enum {
+	kLBEventPhaseInit = 0,
+	kLBEventPhaseIntro = 1,
+	kLBEventMouseDown = 2,
+	kLBEventStarted = 3,
+	kLBEventDone = 4,
+	kLBEventMouseUp = 5,
+	kLBEventPhaseMain = 6,
+	kLBEventNotified = 7,
+	kLBEventDragStart = 8,
+	kLBEventDragMove = 9,
+	kLBEventDragEnd = 0xa,
+	kLBEventRolloverBegin = 0xb,
+	kLBEventRolloverMove = 0xc,
+	kLBEventRolloverEnd = 0xd,
+	kLBEventMouseUpIn = 0xe,
+	kLBEventMouseUpOut = 0xf,
+	kLBEventMouseTrackIn = 0x10,
+	kLBEventMouseTrackMove = 0x11,
+	kLBEventMouseTrackMoveIn = 0x12,
+	kLBEventMouseTrackMoveOut = 0x13,
+	kLBEventMouseTrackOut = 0x14,
+	kLBEventFocusBegin = 0x15,
+	kLBEventFocusEnd = 0x16,
+	kLBEventInit = 0x17,
+	kLBEventLoad = 0x1a,
+	kLBEventListLoad = 0x1b,
+	kLBEventPhaseCreate = 0xff
+};
+
+enum {
+	kLBGroupData = 0x64,
+	kLBLiveTextData = 0x65,
+	kLBMsgListScript = 0x66,
+	kLBNotifyScript = 0x67,
+	kLBSetPlayInfo = 0x68,
+	kLBSetRandomLoc = 0x69,  // unused?
+	kLBSetDrag = 0x6a,       // unused?
+	kLBSetDrawMode = 0x6b,
+	kLBSetFont = 0x6c,       // unused?
+	kLBSetOneShot = 0x6d,    // unused?
+	kLBSetPlayPhase = 0x6e,
+	// from here, 2.x+
+	kLBSetKeyNotify = 0x6f,
+	kLBCommand = 0x70,
+	kLBPaletteAData = 0x71,  // unused?
+	kLBPaletteXData = 0x72,
+	kLBDisable = 0x73,       // unused?
+	kLBEnable = 0x74,        // unused?
+	kLBSetNotVisible = 0x75,
+	kLBSetVisible = 0x76,    // unused?
+	kLBGlobalDisable = 0x77,
+	kLBGlobalEnable = 0x78,  // unused?
+	kLBGlobalSetNotVisible = 0x79,
+	kLBGlobalSetVisible = 0x7a, // unused?
+	kLBSetAmbient = 0x7b,
+	kLBSetDragParams = 0x7c,
+	kLBSetKeyEvent = 0x7d,
+	kLBSetRolloverData = 0x7e,
+	kLBSetParent = 0x7f,
+	kLBSetHitTest = 0x80,
+	// from here, rugrats
+	kLBUnknown194 = 0x194
+};
+
+enum {
+	kLBOpNone = 0x0,
+	kLBOpXShow = 0x1,
+	kLBOpTogglePlay = 0x2,
+	kLBOpSetNotVisible = 0x3,
+	kLBOpSetVisible = 0x4,
+	kLBOpDestroy = 0x5,
+	kLBOpRewind = 0x6,
+	kLBOpStop = 0x7,
+	kLBOpDisable = 0x8,
+	kLBOpEnable = 0x9,
+	// (no 0xa)
+	kLBOpGlobalSetNotVisible = 0xb,
+	kLBOpGlobalSetVisible = 0xc,
+	kLBOpGlobalDisable = 0xd,
+	kLBOpGlobalEnable = 0xe,
+	kLBOpSeekToEnd = 0xf,
+	// (no 0x10)
+	kLBOpMute = 0x11,
+	kLBOpUnmute = 0x12,
+	kLBOpLoad = 0x13,
+	kLBOpPreload = 0x14,
+	kLBOpUnload = 0x15,
+	kLBOpSeekToNext = 0x16,
+	kLBOpSeekToPrev = 0x17,
+	kLBOpDragBegin = 0x18,
+	kLBOpDragEnd = 0x19,
+	kLBOpScriptDisable = 0x1a,
+	kLBOpScriptEnable = 0x1b,
+	kLBOpUnknown1C = 0x1c,
+	kLBOpSendExpression = 0x1d,
+	kLBOpJumpUnlessExpression = 0xfffb,
+	kLBOpBreakExpression = 0xfffc,
+	kLBOpJumpToExpression = 0xfffd,
+	kLBOpRunSubentries = 0xfffe,
+	kLBOpRunData = 0xffff
+};
+
+enum {
+	kLBNotifyGUIAction = 1,
+	kLBNotifyGoToControls = 2,
+	kLBNotifyChangePage = 3,
+	kLBNotifyGotoQuit = 4,
+	kLBNotifyIntroDone = 5,
+	kLBNotifyChangeMode = 6,
+	kLBNotifyCursorChange = 7,
+	kLBNotifyPrintPage = 0xc,
+	kLBNotifyQuit = 0xd
+};
+
+enum {
+	kTargetTypeExpression = 0x3f3f,
+	kTargetTypeCode = 0xfffe,
+	kTargetTypeName = 0xffff
+};
+
+} // End of namespace Mohawk
+
+#endif
diff --git a/engines/mohawk/livingbooks_item.cpp b/engines/mohawk/livingbooks_item.cpp
new file mode 100644
index 00000000000..e6c776d980a
--- /dev/null
+++ b/engines/mohawk/livingbooks_item.cpp
@@ -0,0 +1,343 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mohawk/livingbooks.h"
+#include "mohawk/cursors.h"
+
+#include "common/system.h"
+#include "common/textconsole.h"
+
+namespace Mohawk {
+
+LBItem::LBItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect) : _vm(vm), _page(page), _rect(rect) {
+	if (_vm->getGameType() == GType_LIVINGBOOKSV1 || _vm->getGameType() == GType_LIVINGBOOKSV2)
+		_phase = kLBPhaseInit;
+	else
+		_phase = kLBPhaseLoad;
+
+	_loopMode = 0;
+	_delayMin = 0;
+	_delayMax = 0;
+	_timingMode = kLBAutoNone;
+	_periodMin = 0;
+	_periodMax = 0;
+	_controlMode = kLBControlNone;
+	_soundMode = 0;
+
+	_loaded = false;
+	_enabled = false;
+	_visible = true;
+	_playing = false;
+	_globalEnabled = true;
+	_globalVisible = true;
+	_nextTime = 0;
+	_startTime = 0;
+	_loops = 0;
+
+	_isAmbient = false;
+	_doHitTest = true;
+}
+
+LBItem::~LBItem() {
+	for (uint i = 0; i < _scriptEntries.size(); i++)
+		delete _scriptEntries[i];
+}
+
+void LBItem::destroySelf() {
+	if (!this->_itemId)
+		error("destroySelf() on an item which was already dead");
+
+	_vm->queueDelayedEvent(DelayedEvent(this, kLBDelayedEventDestroy));
+
+	_itemId = 0;
+}
+
+void LBItem::setEnabled(bool enabled) {
+	if (enabled && !_loaded && !_playing) {
+		if (_timingMode == kLBAutoUserIdle) {
+			setNextTime(_periodMin, _periodMax);
+			debug(2, "Enable time startup");
+		}
+	}
+
+	_enabled = enabled;
+}
+
+void LBItem::setGlobalEnabled(bool enabled) {
+	bool wasEnabled = _loaded && _enabled && _globalEnabled;
+	_globalEnabled = enabled;
+	if (wasEnabled != (_loaded && _enabled && _globalEnabled))
+		setEnabled(enabled);
+}
+
+bool LBItem::contains(Common::Point point) {
+	if (!_loaded)
+		return false;
+
+	if (_playing && _loopMode == 0xFFFF)
+		stop();
+
+	if (!_playing && _timingMode == kLBAutoUserIdle)
+		setNextTime(_periodMin, _periodMax);
+
+	return _visible && _globalVisible && _rect.contains(point);
+}
+
+void LBItem::update() {
+	if (_phase != kLBPhaseNone && (!_loaded || !_enabled || !_globalEnabled))
+		return;
+
+	if (_nextTime == 0 || _nextTime > (uint32)(_vm->_system->getMillis() / 16))
+		return;
+
+	if (togglePlaying(_playing, true)) {
+		_nextTime = 0;
+	} else if (_loops == 0 && _timingMode == kLBAutoUserIdle) {
+		debug(9, "Looping in update()");
+		setNextTime(_periodMin, _periodMax);
+	}
+}
+
+void LBItem::handleMouseDown(Common::Point pos) {
+	if (!_loaded || !_enabled || !_globalEnabled)
+		return;
+
+	_vm->setFocus(this);
+	runScript(kLBEventMouseDown);
+	runScript(kLBEventMouseTrackIn);
+}
+
+void LBItem::handleMouseMove(Common::Point pos) {
+	// TODO: handle drag
+}
+
+void LBItem::handleMouseUp(Common::Point pos) {
+	_vm->setFocus(nullptr);
+	runScript(kLBEventMouseUp);
+	runScript(kLBEventMouseUpIn);
+}
+
+bool LBItem::togglePlaying(bool playing, bool restart) {
+	if (playing) {
+		_vm->queueDelayedEvent(DelayedEvent(this, kLBDelayedEventDone));
+		return true;
+	}
+	if (((_loaded && _enabled && _globalEnabled) || _phase == kLBPhaseNone) && !_playing) {
+		_playing = togglePlaying(true, restart);
+		if (_playing) {
+			_nextTime = 0;
+			_startTime = _vm->_system->getMillis() / 16;
+
+			if (_loopMode == 0xFFFF || _loopMode == 0xFFFE)
+				_loops = 0xFFFF;
+			else
+				_loops = _loopMode;
+
+			if (_controlMode >= kLBControlHideMouse) {
+				debug(2, "Hiding cursor");
+				_vm->_cursor->hideCursor();
+				_vm->lockSound(this, true);
+
+				if (_controlMode >= kLBControlPauseItems) {
+					debug(2, "Disabling all");
+					_vm->setEnableForAll(false, this);
+				}
+			}
+
+			runScript(kLBEventStarted);
+			notify(0, _itemId);
+		}
+	}
+	return _playing;
+}
+
+void LBItem::done(bool onlyNotify) {
+	if (onlyNotify) {
+		if (_relocPoint.x || _relocPoint.y) {
+			_rect.translate(_relocPoint.x, _relocPoint.y);
+			// TODO: does drag box need adjusting?
+		}
+
+		if (_loops && --_loops) {
+			debug(9, "Real looping (now 0x%04x left)", _loops);
+			setNextTime(_delayMin, _delayMax, _startTime);
+		} else
+			done(false);
+
+		return;
+	}
+
+	_playing = false;
+	_loops = 0;
+	_startTime = 0;
+
+	if (_controlMode >= kLBControlHideMouse) {
+		debug(2, "Showing cursor");
+		_vm->_cursor->showCursor();
+		_vm->lockSound(this, false);
+
+		if (_controlMode >= kLBControlPauseItems) {
+			debug(2, "Enabling all");
+			_vm->setEnableForAll(true, this);
+		}
+	}
+
+	if (_timingMode == kLBAutoUserIdle) {
+		debug(9, "Looping in done() - %d to %d", _periodMin, _periodMax);
+		setNextTime(_periodMin, _periodMax);
+	}
+
+	runScript(kLBEventDone);
+	notify(0xFFFF, _itemId);
+}
+
+void LBItem::init() {
+	runScript(kLBEventInit);
+}
+
+void LBItem::setVisible(bool visible) {
+	if (visible == _visible)
+		return;
+
+	_visible = visible;
+	_vm->_needsRedraw = true;
+}
+
+void LBItem::setGlobalVisible(bool visible) {
+	bool wasEnabled = _visible && _globalVisible;
+	_globalVisible = visible;
+	if (wasEnabled != (_visible && _globalVisible))
+		_vm->_needsRedraw = true;
+}
+
+void LBItem::startPhase(uint phase) {
+	if (_phase == phase) {
+		if (_phase != kLBPhaseNone) {
+			setEnabled(true);
+		}
+
+		load();
+	}
+
+	switch (phase) {
+	case kLBPhaseLoad:
+		runScript(kLBEventListLoad);
+		break;
+	case kLBPhaseCreate:
+		runScript(kLBEventPhaseCreate);
+		if (_timingMode == kLBAutoCreate) {
+			debug(2, "Phase create: time startup");
+			setNextTime(_periodMin, _periodMax);
+		}
+		break;
+	case kLBPhaseInit:
+		runScript(kLBEventPhaseInit);
+		if (_timingMode == kLBAutoInit) {
+			debug(2, "Phase init: time startup");
+			setNextTime(_periodMin, _periodMax);
+		}
+		break;
+	case kLBPhaseIntro:
+		runScript(kLBEventPhaseIntro);
+		if (_timingMode == kLBAutoIntro || _timingMode == kLBAutoUserIdle) {
+			debug(2, "Phase intro: time startup");
+			setNextTime(_periodMin, _periodMax);
+		}
+		break;
+	case kLBPhaseMain:
+		runScript(kLBEventPhaseMain);
+		if (_timingMode == kLBAutoUserIdle || _timingMode == kLBAutoMain) {
+			debug(2, "Phase main: time startup");
+			setNextTime(_periodMin, _periodMax);
+		}
+		break;
+	default:
+		break;
+	}
+}
+
+void LBItem::stop() {
+	if (!_playing)
+		return;
+
+	_loops = 0;
+	seek(0xFFFF);
+	done(true);
+}
+
+void LBItem::load() {
+	if (_loaded)
+		return;
+
+	_loaded = true;
+
+	// FIXME: events etc
+	if (_timingMode == kLBAutoLoad) {
+		debug(2, "Load: time startup");
+		setNextTime(_periodMin, _periodMax);
+	}
+}
+
+void LBItem::unload() {
+	if (!_loaded)
+		return;
+
+	_loaded = false;
+
+	// FIXME: stuff
+}
+
+void LBItem::moveBy(const Common::Point &pos) {
+	_rect.translate(pos.x, pos.y);
+}
+
+void LBItem::moveTo(const Common::Point &pos) {
+	_rect.moveTo(pos);
+}
+
+LBItem *LBItem::clone(uint16 newId, const Common::String &newName) {
+	LBItem *item = createClone();
+
+	item->_itemId = newId;
+	item->_desc = newName;
+
+	item->_resourceId = _resourceId;
+	// FIXME: the rest
+
+	_page->addClonedItem(item);
+	// FIXME: zorder?
+	return item;
+}
+
+LBItem *LBItem::createClone() {
+	return new LBItem(_vm, _page, _rect);
+}
+
+void LBItem::setNextTime(uint16 min, uint16 max) {
+	setNextTime(min, max, _vm->_system->getMillis() / 16);
+}
+
+void LBItem::setNextTime(uint16 min, uint16 max, uint32 start) {
+	_nextTime = start + _vm->_rnd->getRandomNumberRng((uint)min, (uint)max);
+	debug(9, "nextTime is now %d frames away", _nextTime - (uint)(_vm->_system->getMillis() / 16));
+}
+
+} // End of namespace Mohawk
diff --git a/engines/mohawk/livingbooks_item.h b/engines/mohawk/livingbooks_item.h
new file mode 100644
index 00000000000..448bdf5a942
--- /dev/null
+++ b/engines/mohawk/livingbooks_item.h
@@ -0,0 +1,135 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MOHAWK_LIVINGBOOKS_ITEM_H
+#define MOHAWK_LIVINGBOOKS_ITEM_H
+
+#include "livingbooks_code.h"
+
+#include "common/array.h"
+#include "common/hash-str.h"
+#include "common/hashmap.h"
+#include "common/list.h"
+#include "common/rect.h"
+#include "common/str.h"
+
+namespace Common {
+	class SeekableReadStreamEndian;
+	class MemoryReadStreamEndian;
+}
+
+namespace Mohawk {
+
+class MohawkEngine_LivingBooks;
+class LBPage;
+struct LBScriptEntry;
+
+class LBItem {
+	friend class LBCode;
+
+public:
+	LBItem(MohawkEngine_LivingBooks *vm, LBPage *page, Common::Rect rect);
+	virtual ~LBItem();
+
+	void readFrom(Common::SeekableReadStreamEndian *stream);
+	void readData(uint16 type, uint16 size, byte *data);
+	virtual void readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream);
+
+	virtual void destroySelf(); // 0x2
+	virtual void setEnabled(bool enabled); // 0x3
+	virtual void setGlobalEnabled(bool enabled);
+	virtual bool contains(Common::Point point); // 0x7
+	virtual void update(); // 0x8
+	virtual void draw() { } // 0x9
+	virtual void handleKeyChar(Common::Point pos) { } // 0xA
+	virtual void handleMouseDown(Common::Point pos); // 0xB
+	virtual void handleMouseMove(Common::Point pos); // 0xC
+	virtual void handleMouseUp(Common::Point pos); // 0xD
+	virtual bool togglePlaying(bool playing, bool restart = false); // 0xF
+	virtual void done(bool onlyNotify); // 0x10
+	virtual void init(); // 0x11
+	virtual void seek(uint16 pos) { } // 0x13
+	virtual void seekToTime(uint32 time) { }
+	virtual void setFocused(bool focused) { } // 0x14
+	virtual void setVisible(bool visible); // 0x17
+	virtual void setGlobalVisible(bool enabled);
+	virtual void startPhase(uint phase); // 0x18
+	virtual void stop(); // 0x19
+	virtual void notify(uint16 data, uint16 from); // 0x1A
+	virtual void load();
+	virtual void unload();
+	virtual void moveBy(const Common::Point &pos);
+	virtual void moveTo(const Common::Point &pos);
+
+	LBItem *clone(uint16 newId, const Common::String &newName);
+
+	uint16 getId() { return _itemId; }
+	const Common::String &getName() { return _desc; }
+	const Common::Rect &getRect() { return _rect; }
+	uint16 getSoundPriority() { return _soundMode; }
+	bool isLoaded() { return _loaded; }
+	bool isAmbient() { return _isAmbient; }
+
+	Common::List<LBItem *>::iterator _iterator;
+
+	// TODO: make private
+	Common::HashMap<Common::String, LBValue, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> _variables;
+
+protected:
+	MohawkEngine_LivingBooks *_vm;
+	LBPage *_page;
+
+	void setNextTime(uint16 min, uint16 max);
+	void setNextTime(uint16 min, uint16 max, uint32 start);
+
+	Common::Rect _rect;
+	Common::String _desc;
+	uint16 _resourceId;
+	uint16 _itemId;
+
+	bool _loaded, _visible, _globalVisible, _playing, _enabled, _globalEnabled;
+
+	uint32 _nextTime, _startTime;
+	uint16 _loops;
+
+	uint16 _phase, _timingMode, _delayMin, _delayMax;
+	uint16 _loopMode, _periodMin, _periodMax;
+	uint16 _controlMode, _soundMode;
+	Common::Point _relocPoint;
+
+	bool _isAmbient;
+	bool _doHitTest;
+
+	virtual LBItem *createClone();
+
+	Common::Array<LBScriptEntry *> _scriptEntries;
+	void runScript(uint event, uint16 data = 0, uint16 from = 0);
+	int runScriptEntry(LBScriptEntry *entry);
+
+	void runCommand(const Common::String &command);
+	bool checkCondition(const Common::String &condition);
+
+	LBScriptEntry *parseScriptEntry(uint16 type, uint16 &size, Common::MemoryReadStreamEndian *stream, bool isSubentry = false);
+};
+
+} // End of namespace Mohawk
+
+#endif
diff --git a/engines/mohawk/livingbooks_itemscript.cpp b/engines/mohawk/livingbooks_itemscript.cpp
new file mode 100644
index 00000000000..73f83f50d5e
--- /dev/null
+++ b/engines/mohawk/livingbooks_itemscript.cpp
@@ -0,0 +1,834 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mohawk/livingbooks.h"
+
+#include "common/memstream.h"
+#include "common/textconsole.h"
+
+namespace Mohawk {
+
+// Not to be confused with SCRP as "scripts". SCRP is for LBAnimation
+
+LBScriptEntry::LBScriptEntry() {
+	state = 0;
+	data = nullptr;
+	argvParam = nullptr;
+	argvTarget = nullptr;
+}
+
+LBScriptEntry::~LBScriptEntry() {
+	delete[] argvParam;
+	delete[] argvTarget;
+	delete[] data;
+
+	for (uint i = 0; i < subentries.size(); i++)
+		delete subentries[i];
+}
+
+void LBItem::readFrom(Common::SeekableReadStreamEndian *stream) {
+	_resourceId = stream->readUint16();
+	_itemId = stream->readUint16();
+	uint16 size = stream->readUint16();
+	_desc = _vm->readString(stream);
+
+	debug(2, "Item: size %d, resource %d, id %d", size, _resourceId, _itemId);
+	debug(2, "Coords: %d, %d, %d, %d", _rect.left, _rect.top, _rect.right, _rect.bottom);
+	debug(2, "String: '%s'", _desc.c_str());
+
+	if (!_itemId)
+		error("Item had invalid item id");
+
+	int endPos = stream->pos() + size;
+	if (endPos > stream->size())
+		error("Item is larger (should end at %d) than stream (size %d)", endPos, (int)stream->size());
+
+	while (true) {
+		if (stream->pos() == endPos)
+			break;
+
+		uint oldPos = stream->pos();
+
+		uint16 dataType = stream->readUint16();
+		uint16 dataSize = stream->readUint16();
+
+		debug(4, "Data type %04x, size %d", dataType, dataSize);
+		byte *buf = new byte[dataSize];
+		stream->read(buf, dataSize);
+		readData(dataType, dataSize, buf);
+		delete[] buf;
+
+		if ((uint)stream->pos() != oldPos + 4 + (uint)dataSize)
+			error("Failed to read correct number of bytes (off by %d) for data type %04x (size %d)",
+				(int)stream->pos() - (int)(oldPos + 4 + (uint)dataSize), dataType, dataSize);
+
+		if (stream->pos() > endPos)
+			error("Read off the end (at %d) of data (ends at %d)", (int)stream->pos(), endPos);
+
+		assert(!stream->eos());
+	}
+}
+
+LBScriptEntry *LBItem::parseScriptEntry(uint16 type, uint16 &size, Common::MemoryReadStreamEndian *stream, bool isSubentry) {
+	if (size < 6)
+		error("Script entry of type 0x%04x was too small (%d)", type, size);
+
+	uint16 expectedEndSize = 0;
+
+	LBScriptEntry *entry = new LBScriptEntry;
+	entry->type = type;
+	if (isSubentry) {
+		expectedEndSize = size - (stream->readUint16() + 2);
+		entry->event = 0xffff;
+	} else
+		entry->event = stream->readUint16();
+	entry->opcode = stream->readUint16();
+	entry->param = stream->readUint16();
+	debug(4, "Script entry: type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x",
+			entry->type, entry->event, entry->opcode, entry->param);
+	size -= 6;
+
+	// TODO: read as bytes, if this is correct (but beware endianism)
+	byte conditionTag = (entry->event & 0xff00) >> 8;
+	entry->event = entry->event & 0xff;
+
+	if (type == kLBMsgListScript && entry->opcode == kLBOpRunSubentries) {
+		debug(4, "%d script subentries:", entry->param);
+		entry->argc = 0;
+		for (uint i = 0; i < entry->param; i++) {
+			LBScriptEntry *subentry = parseScriptEntry(type, size, stream, true);
+			entry->subentries.push_back(subentry);
+
+			// subentries are aligned
+			if (i + 1 < entry->param && size % 2 == 1) {
+				stream->skip(1);
+				size--;
+			}
+		}
+	} else if (type == kLBMsgListScript) {
+		if (size < 2)
+			error("Script entry of type 0x%04x was too small (%d)", type, size);
+
+		entry->argc = stream->readUint16();
+		size -= 2;
+
+		entry->targetingType = 0;
+
+		uint16 targetingType = entry->argc;
+		if (targetingType == kTargetTypeExpression || targetingType == kTargetTypeCode
+			|| targetingType == kTargetTypeName) {
+			entry->targetingType = targetingType;
+
+			// FIXME
+			if (targetingType == kTargetTypeCode)
+				error("encountered kTargetTypeCode");
+
+			if (size < 2)
+				error("not enough bytes (%d) reading special targeting", size);
+			uint16 count = stream->readUint16();
+			size -= 2;
+
+			debug(4, "%d targets with targeting type %04x", count, targetingType);
+
+			uint oldAlign = size % 2;
+			for (uint i = 0; i < count; i++) {
+				Common::String target = _vm->readString(stream);
+				debug(4, "target '%s'", target.c_str());
+				entry->targets.push_back(target);
+				if (target.size() + 1 > size)
+					error("failed to read target (ran out of stream)");
+				size -= target.size() + 1;
+			}
+			entry->argc = entry->targets.size();
+
+			if ((uint)(size % 2) != oldAlign) {
+				stream->skip(1);
+				size--;
+			}
+		} else if (entry->argc) {
+			entry->argvParam = new uint16[entry->argc];
+			entry->argvTarget = new uint16[entry->argc];
+			debug(4, "With %d targets:", entry->argc);
+
+			if (size < (entry->argc * 4))
+				error("Script entry of type 0x%04x was too small (%d)", type, size);
+
+			for (uint i = 0; i < entry->argc; i++) {
+				entry->argvParam[i] = stream->readUint16();
+				entry->argvTarget[i] = stream->readUint16();
+				debug(4, "Target %d, param 0x%04x", entry->argvTarget[i], entry->argvParam[i]);
+			}
+
+			size -= (entry->argc * 4);
+		}
+	}
+
+	if (type == kLBMsgListScript && entry->opcode == kLBOpJumpUnlessExpression) {
+		if (size < 6)
+			error("not enough bytes (%d) in kLBOpJumpUnlessExpression, event 0x%04x", size, entry->event);
+		entry->offset = stream->readUint32();
+		entry->target = stream->readUint16();
+		debug(4, "kLBOpJumpUnlessExpression: offset %08x, target %d", entry->offset, entry->target);
+		size -= 6;
+	}
+	if (type == kLBMsgListScript && entry->opcode == kLBOpJumpToExpression) {
+		if (size < 4)
+			error("not enough bytes (%d) in kLBOpJumpToExpression, event 0x%04x", size, entry->event);
+		entry->offset = stream->readUint32();
+		debug(4, "kLBOpJumpToExpression: offset %08x", entry->offset);
+		size -= 4;
+	}
+
+	if (type == kLBNotifyScript && entry->opcode == kLBNotifyChangeMode && _vm->getGameType() != GType_LIVINGBOOKSV1) {
+		switch (entry->param) {
+		case 1:
+			if (size < 8)
+				error("%d unknown bytes in notify entry kLBNotifyChangeMode", size);
+			entry->newUnknown = stream->readUint16();
+			entry->newMode = stream->readUint16();
+			entry->newPage = stream->readUint16();
+			entry->newSubpage = stream->readUint16();
+			debug(4, "kLBNotifyChangeMode: unknown %04x, mode %d, page %d.%d",
+				entry->newUnknown, entry->newMode, entry->newPage, entry->newSubpage);
+			size -= 8;
+			break;
+		case 3:
+			{
+			Common::String newCursor = _vm->readString(stream);
+			entry->newCursor = newCursor;
+			if (size < newCursor.size() + 1)
+				error("failed to read newCursor in notify entry");
+			size -= newCursor.size() + 1;
+			debug(4, "kLBNotifyChangeMode: new cursor '%s'", newCursor.c_str());
+			}
+			break;
+		default:
+			// the original engine also does something when param==2 (but not a notify)
+			error("unknown v2 kLBNotifyChangeMode type %d", entry->param);
+		}
+	}
+	if (entry->opcode == kLBOpSendExpression) {
+		if (size < 4)
+			error("not enough bytes (%d) in kLBOpSendExpression, event 0x%04x", size, entry->event);
+		entry->offset = stream->readUint32();
+		debug(4, "kLBOpSendExpression: offset %08x", entry->offset);
+		size -= 4;
+	}
+	if (entry->opcode == kLBOpRunData) {
+		if (size < 4)
+			error("didn't get enough bytes (%d) to read data header in script entry", size);
+		entry->dataType = stream->readUint16();
+		entry->dataLen = stream->readUint16();
+		size -= 4;
+
+		if (size < entry->dataLen)
+			error("didn't get enough bytes (%d) to read data in script entry", size);
+
+		if (entry->dataType == kLBCommand) {
+			Common::String command = _vm->readString(stream);
+			uint commandSize = command.size() + 1;
+			if (commandSize > entry->dataLen)
+				error("failed to read command in script entry: dataLen %d, command '%s' (%d chars)",
+					 entry->dataLen, command.c_str(), commandSize);
+			entry->dataLen = commandSize;
+			entry->data = new byte[commandSize];
+			memcpy(entry->data, command.c_str(), commandSize);
+			size -= commandSize;
+		} else {
+			if (conditionTag)
+				error("kLBOpRunData had unexpected conditionTag");
+			entry->data = new byte[entry->dataLen];
+			stream->read(entry->data, entry->dataLen);
+			size -= entry->dataLen;
+		}
+	}
+	if (entry->event == kLBEventNotified) {
+		if (size < 4)
+			error("not enough bytes (%d) in kLBEventNotified, opcode 0x%04x", size, entry->opcode);
+		entry->matchFrom = stream->readUint16();
+		entry->matchNotify = stream->readUint16();
+		debug(4, "kLBEventNotified: matches %04x (from %04x)",
+			entry->matchNotify, entry->matchFrom);
+		size -= 4;
+	}
+
+	if (isSubentry) {
+		// TODO: subentries may be aligned, so this check is a bit too relaxed
+		if (size != expectedEndSize && size != expectedEndSize + 1)
+			error("expected %d bytes left at end of subentry, but had %d",
+				expectedEndSize, size);
+		return entry;
+	}
+
+	if (conditionTag == 1) {
+		if (!size)
+			error("failed to read condition (empty stream)");
+		Common::String condition = _vm->readString(stream);
+		if (condition.size() == 0) {
+			size--;
+			if (!size)
+				error("failed to read condition (null byte, then ran out of stream)");
+			condition = _vm->readString(stream);
+		}
+		if (condition.size() + 1 > size)
+			error("failed to read condition (ran out of stream)");
+		size -= (condition.size() + 1);
+
+		entry->conditions.push_back(condition);
+		debug(4, "script entry condition '%s'", condition.c_str());
+	} else if (conditionTag == 2) {
+		if (size < 4)
+			error("expected more than %d bytes for conditionTag 2", size);
+		// FIXME
+		stream->skip(4);
+		size -= 4;
+	}
+
+	if (size == 1) {
+		// FIXME: this is alignment, but why?
+		stream->skip(1);
+		size--;
+	} else if (size)
+		error("failed to read script entry correctly (%d bytes left): type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x",
+			size, entry->type, entry->event, entry->opcode, entry->param);
+
+	return entry;
+}
+
+void LBItem::readData(uint16 type, uint16 size, byte *data) {
+	Common::MemoryReadStreamEndian stream(data, size, _vm->isBigEndian());
+	readData(type, size, &stream);
+}
+
+void LBItem::readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) {
+	switch (type) {
+	case kLBMsgListScript:
+	case kLBNotifyScript:
+		_scriptEntries.push_back(parseScriptEntry(type, size, stream));
+		break;
+
+	case kLBSetPlayInfo:
+		{
+		if (size != 20)
+			error("kLBSetPlayInfo had wrong size (%d)", size);
+
+		_loopMode = stream->readUint16();
+		_delayMin = stream->readUint16();
+		_delayMax = stream->readUint16();
+		_timingMode = stream->readUint16();
+		if (_timingMode > 7)
+			error("encountered timing mode %04x", _timingMode);
+		_periodMin = stream->readUint16();
+		_periodMax = stream->readUint16();
+		_relocPoint.x = stream->readSint16();
+		_relocPoint.y = stream->readSint16();
+		_controlMode = stream->readUint16();
+		_soundMode = stream->readUint16();
+
+		debug(2, "kLBSetPlayInfo: loop mode %d (%d to %d), timing mode %d (%d to %d), reloc (%d, %d), control mode %04x, sound mode %04x",
+			_loopMode, _delayMin, _delayMax,
+			_timingMode, _periodMin, _periodMax,
+			_relocPoint.x, _relocPoint.y,
+			_controlMode, _soundMode);
+		}
+		break;
+
+	case kLBSetPlayPhase:
+		if (size != 2)
+			error("SetPlayPhase had wrong size (%d)", size);
+		_phase = stream->readUint16();
+		debug(2, "kLBSetPlayPhase: %d", _phase);
+		break;
+
+	case kLBSetKeyNotify:
+		{
+		// FIXME: variable-size notifies, targets
+		if (size != 18)
+			error("0x6f had wrong size (%d)", size);
+		uint event = stream->readUint16();
+		LBKey key;
+		stream->read(&key, 4);
+		uint opcode = stream->readUint16();
+		uint param = stream->readUint16();
+		uint u6 = stream->readUint16();
+		uint u7 = stream->readUint16();
+		uint u8 = stream->readUint16();
+		uint u9 = stream->readUint16();
+		warning("ignoring kLBSetKeyNotify: item %s, key code %02x (modifier mask %d, char %d, repeat %d), event %04x, opcode %04x, param %04x, unknowns %04x, %04x, %04x, %04x",
+			_desc.c_str(), key.code, key.modifiers, key.char_, key.repeats, event, opcode, param, u6, u7, u8, u9);
+		}
+		break;
+
+	case kLBCommand:
+		{
+			Common::String command = _vm->readString(stream);
+			if (size != command.size() + 1)
+				error("failed to read command string");
+
+			runCommand(command);
+		}
+		break;
+
+	case kLBSetNotVisible:
+		assert(size == 0);
+		_visible = false;
+		break;
+
+	case kLBGlobalDisable:
+		assert(size == 0);
+		_globalEnabled = false;
+		break;
+
+	case kLBGlobalSetNotVisible:
+		assert(size == 0);
+		_globalVisible = false;
+		break;
+
+	case kLBSetAmbient:
+		assert(size == 0);
+		_isAmbient = true;
+		break;
+
+	case kLBSetKeyEvent:
+		{
+		// FIXME: targets
+		if (size != 10)
+			error("kLBSetKeyEvent had wrong size (%d)", size);
+		uint u3 = stream->readUint16();
+		LBKey key;
+		stream->read(&key, 4);
+		uint target = stream->readUint16();
+		uint16 event = stream->readUint16();
+		// FIXME: this is scripting stuff: what to run when key is pressed
+		warning("ignoring kLBSetKeyEvent: item %s, key code %02x (modifier mask %d, char %d, repeat %d) unknown %04x, target %d, event %04x",
+			_desc.c_str(), key.code, key.modifiers, key.char_, key.repeats, u3, target, event);
+		}
+		break;
+
+	case kLBSetHitTest:
+		{
+		assert(size == 2);
+		uint val = stream->readUint16();
+		_doHitTest = (bool)val;
+		debug(2, "kLBSetHitTest (on %s): value %04x", _desc.c_str(), val);
+		}
+		break;
+
+	case kLBSetRolloverData:
+		{
+		assert(size == 2);
+		uint16 flag = stream->readUint16();
+		warning("ignoring kLBSetRolloverData: item %s, flag %d", _desc.c_str(), flag);
+		}
+		break;
+
+	case kLBSetParent:
+		{
+		assert(size == 2);
+		uint16 parent = stream->readUint16();
+		warning("ignoring kLBSetParent: item %s, parent id %d", _desc.c_str(), parent);
+		}
+		break;
+
+	case kLBUnknown194:
+		{
+		assert(size == 4);
+		uint offset = stream->readUint32();
+		_page->_code->runCode(this, offset);
+		}
+		break;
+
+	default:
+		error("Unknown message %04x (size 0x%04x)", type, size);
+		//for (uint i = 0; i < size; i++)
+		//	debugN("%02x ", stream->readByte());
+		//debugN("\n");
+		break;
+	}
+}
+
+void LBItem::notify(uint16 data, uint16 from) {
+	if (_timingMode == kLBAutoSync) {
+		// TODO: is this correct?
+		if (_periodMin == data && _periodMax == from) {
+			debug(2, "Handling notify 0x%04x (from %d)", data, from);
+			setNextTime(0, 0);
+		}
+	}
+
+	runScript(kLBEventNotified, data, from);
+}
+
+void LBItem::runScript(uint event, uint16 data, uint16 from) {
+	for (uint i = 0; i < _scriptEntries.size(); i++) {
+		LBScriptEntry *entry = _scriptEntries[i];
+
+		if (entry->event != event)
+			continue;
+
+		if (event == kLBEventNotified) {
+			if ((entry->matchFrom && entry->matchFrom != from) || entry->matchNotify != data)
+				continue;
+		}
+
+		bool conditionsMatch = true;
+		for (uint n = 0; n < entry->conditions.size(); n++) {
+			if (!checkCondition(entry->conditions[n])) {
+				conditionsMatch = false;
+				break;
+			}
+		}
+		if (!conditionsMatch)
+			continue;
+
+		if (entry->type == kLBNotifyScript) {
+			debug(2, "Notify: event 0x%04x, opcode 0x%04x, param 0x%04x",
+				entry->event, entry->opcode, entry->param);
+
+			if (entry->opcode == kLBNotifyGUIAction)
+				_vm->addNotifyEvent(NotifyEvent(entry->opcode, _itemId));
+			else if (entry->opcode == kLBNotifyChangeMode && _vm->getGameType() != GType_LIVINGBOOKSV1) {
+				NotifyEvent notifyEvent(entry->opcode, entry->param);
+				notifyEvent.newUnknown = entry->newUnknown;
+				notifyEvent.newMode = entry->newMode;
+				notifyEvent.newPage = entry->newPage;
+				notifyEvent.newSubpage = entry->newSubpage;
+				notifyEvent.newCursor = entry->newCursor;
+				_vm->addNotifyEvent(notifyEvent);
+			} else
+				_vm->addNotifyEvent(NotifyEvent(entry->opcode, entry->param));
+		} else
+			runScriptEntry(entry);
+	}
+}
+
+int LBItem::runScriptEntry(LBScriptEntry *entry) {
+	if (entry->state == 0xffff)
+		return 0;
+
+	uint start = 0;
+	uint count = entry->argc;
+	// zero targets = apply to self
+	if (!count)
+		count = 1;
+
+	if (entry->opcode != kLBOpRunSubentries) switch (entry->param) {
+	case 0xfffe:
+		// Run once (disable self after run).
+		entry->state = 0xffff;
+		break;
+	case 0xffff:
+		break;
+	case 0:
+	case 1:
+	case 2:
+		start = entry->state;
+		entry->state++;
+		if (entry->state >= count) {
+			switch (entry->param) {
+			case 0:
+				// Disable..
+				entry->state = 0xffff;
+				return 0;
+			case 1:
+				// Stay at the end.
+				entry->state = count - 1;
+				break;
+			case 2:
+				// Loop.
+				entry->state = 0;
+				break;
+			default:
+				break;
+			}
+		}
+		count = 1;
+		break;
+	case 3:
+		// Pick random target.
+		start = _vm->_rnd->getRandomNumberRng(0, count);
+		count = 1;
+		break;
+	default:
+		warning("Weird param for script entry (type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x)",
+			entry->type, entry->event, entry->opcode, entry->param);
+	}
+
+	for (uint n = start; n < count; n++) {
+		LBItem *target;
+
+		debug(2, "Script run: type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x",
+			entry->type, entry->event, entry->opcode, entry->param);
+
+		if (entry->argc) {
+			switch (entry->targetingType) {
+			case kTargetTypeExpression:
+				{
+				// FIXME: this should be EVALUATED
+				LBValue &tgt = _vm->_variables[entry->targets[n]];
+				switch (tgt.type) {
+				case kLBValueItemPtr:
+					target = tgt.item;
+					break;
+				case kLBValueString:
+					// FIXME: handle 'self', at least
+					// TODO: correct otherwise? or only self?
+					target = _vm->getItemByName(tgt.string);
+					break;
+				case kLBValueInteger:
+					target = _vm->getItemById(tgt.integer);
+					break;
+				default:
+					// FIXME: handle list
+					warning("Target '%s' (by expression) resulted in unknown type, skipping", entry->targets[n].c_str());
+					continue;
+				}
+				}
+				if (!target) {
+					debug(2, "Target '%s' (by expression) doesn't exist, skipping", entry->targets[n].c_str());
+					continue;
+				}
+				debug(2, "Target: '%s' (expression '%s')", target->_desc.c_str(), entry->targets[n].c_str());
+				break;
+			case kTargetTypeCode:
+				// FIXME
+				error("encountered kTargetTypeCode");
+				break;
+			case kTargetTypeName:
+				// FIXME: handle 'self'
+				target = _vm->getItemByName(entry->targets[n]);
+				if (!target) {
+					debug(2, "Target '%s' (by name) doesn't exist, skipping", entry->targets[n].c_str());
+					continue;
+				}
+				debug(2, "Target: '%s' (by name)", target->_desc.c_str());
+				break;
+			default:
+				uint16 targetId = entry->argvTarget[n];
+				// TODO: is this type, perhaps?
+				uint16 param = entry->argvParam[n];
+				target = _vm->getItemById(targetId);
+				if (!target) {
+					debug(2, "Target %04x (%04x) doesn't exist, skipping", targetId, param);
+					continue;
+				}
+				debug(2, "Target: %04x (%04x) '%s'", targetId, param, target->_desc.c_str());
+			}
+		} else {
+			target = this;
+			debug(2, "Self-target on '%s'", _desc.c_str());
+		}
+
+		// an opcode in the form 0x1xx means to run the script for event 0xx
+		if ((entry->opcode & 0xff00) == 0x0100) {
+			// FIXME: pass on param
+			target->runScript(entry->opcode & 0xff);
+			break;
+		}
+
+		switch (entry->opcode) {
+		case kLBOpNone:
+			warning("ignoring kLBOpNone (event 0x%04x, param 0x%04x, target '%s')",
+					entry->event, entry->param, target->_desc.c_str());
+			break;
+
+		case kLBOpXShow:
+			// TODO: should be setVisible(true) - not a delayed event -
+			// when we're doing the param 1/2/3 stuff above?
+			// and in modern LB this is perhaps just a direct target->setVisible(true)..
+			if (_vm->getGameType() != GType_LIVINGBOOKSV1)
+				warning("kLBOpXShow on '%s' is probably broken", target->_desc.c_str());
+			_vm->queueDelayedEvent(DelayedEvent(this, kLBDelayedEventSetNotVisible));
+			break;
+
+		case kLBOpTogglePlay:
+			target->togglePlaying(false, true);
+			break;
+
+		case kLBOpSetNotVisible:
+			target->setVisible(false);
+			break;
+
+		case kLBOpSetVisible:
+			target->setVisible(true);
+			break;
+
+		case kLBOpDestroy:
+			target->destroySelf();
+			break;
+
+		case kLBOpRewind:
+			target->seek(1);
+			break;
+
+		case kLBOpStop:
+			target->stop();
+			break;
+
+		case kLBOpDisable:
+			target->setEnabled(false);
+			break;
+
+		case kLBOpEnable:
+			target->setEnabled(true);
+			break;
+
+		case kLBOpGlobalSetNotVisible:
+			target->setGlobalVisible(false);
+			break;
+
+		case kLBOpGlobalSetVisible:
+			target->setGlobalVisible(true);
+			break;
+
+		case kLBOpGlobalDisable:
+			target->setGlobalEnabled(false);
+			break;
+
+		case kLBOpGlobalEnable:
+			target->setGlobalEnabled(true);
+			break;
+
+		case kLBOpSeekToEnd:
+			target->seek(0xFFFF);
+			break;
+
+		case kLBOpMute:
+		case kLBOpUnmute:
+			// FIXME
+			warning("ignoring kLBOpMute/Unmute (event 0x%04x, param 0x%04x, target '%s')",
+				entry->event, entry->param, target->_desc.c_str());
+			break;
+
+		case kLBOpLoad:
+			target->load();
+			break;
+
+		case kLBOpPreload:
+			// FIXME
+			warning("ignoring kLBOpPreload (event 0x%04x, param 0x%04x, target '%s')",
+				entry->event, entry->param, target->_desc.c_str());
+			break;
+
+		case kLBOpUnload:
+			target->unload();
+			break;
+
+		case kLBOpSeekToPrev:
+		case kLBOpSeekToNext:
+			// FIXME
+			warning("ignoring kLBOpSeekToPrev/Next (event 0x%04x, param 0x%04x, target '%s')",
+				entry->event, entry->param, target->_desc.c_str());
+			break;
+
+		case kLBOpDragBegin:
+		case kLBOpDragEnd:
+			// FIXME
+			warning("ignoring kLBOpDragBegin/End (event 0x%04x, param 0x%04x, target '%s')",
+				entry->event, entry->param, target->_desc.c_str());
+			break;
+
+		case kLBOpScriptDisable:
+		case kLBOpScriptEnable:
+			// FIXME
+			warning("ignoring kLBOpScriptDisable/Enable (event 0x%04x, param 0x%04x, target '%s')",
+				entry->event, entry->param, target->_desc.c_str());
+			break;
+
+		case kLBOpUnknown1C:
+			// FIXME
+			warning("ignoring kLBOpUnknown1C (event 0x%04x, param 0x%04x, target '%s')",
+				entry->event, entry->param, target->_desc.c_str());
+			break;
+
+		case kLBOpSendExpression:
+			_page->_code->runCode(this, entry->offset);
+			break;
+
+		case kLBOpRunSubentries:
+			for (uint i = 0; i < entry->subentries.size(); i++) {
+				LBScriptEntry *subentry = entry->subentries[i];
+
+				int e = runScriptEntry(subentry);
+
+				switch (subentry->opcode) {
+				case kLBOpJumpUnlessExpression:
+					debug(2, "JumpUnless got %d (to %d, on %d, of %d)", e, subentry->target, i, entry->subentries.size());
+					if (!e)
+						i = subentry->target - 1;
+					break;
+				case kLBOpBreakExpression:
+					debug(2, "BreakExpression");
+					i = entry->subentries.size();
+					break;
+				case kLBOpJumpToExpression:
+					debug(2, "JumpToExpression got %d (on %d, of %d)", e, i, entry->subentries.size());
+					i = e - 1;
+					break;
+				default:
+					break;
+				}
+			}
+			break;
+
+		case kLBOpRunData:
+			readData(entry->dataType, entry->dataLen, entry->data);
+			break;
+
+		case kLBOpJumpUnlessExpression:
+		case kLBOpBreakExpression:
+		case kLBOpJumpToExpression:
+			{
+			LBValue r = _page->_code->runCode(this, entry->offset);
+			// FIXME
+			return r.integer;
+			}
+
+		default:
+			error("Unknown script opcode (type 0x%04x, event 0x%04x, opcode 0x%04x, param 0x%04x, target '%s')",
+				entry->type, entry->event, entry->opcode, entry->param, target->_desc.c_str());
+		}
+	}
+
+	return 0;
+}
+
+void LBItem::runCommand(const Common::String &command) {
+	LBCode tempCode(_vm, 0);
+
+	debug(2, "running command '%s'", command.c_str());
+
+	uint offset = tempCode.parseCode(command);
+	tempCode.runCode(this, offset);
+}
+
+bool LBItem::checkCondition(const Common::String &condition) {
+	LBCode tempCode(_vm, 0);
+
+	debug(3, "checking condition '%s'", condition.c_str());
+
+	uint offset = tempCode.parseCode(condition);
+	LBValue result = tempCode.runCode(this, offset);
+
+	return result.toInt();
+}
+
+} // End of namespace Mohawk
diff --git a/engines/mohawk/livingbooks_itemscript.h b/engines/mohawk/livingbooks_itemscript.h
new file mode 100644
index 00000000000..620d241aa45
--- /dev/null
+++ b/engines/mohawk/livingbooks_itemscript.h
@@ -0,0 +1,82 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MOHAWK_LIVINGBOOKS_ITEMSCRIPT_H
+#define MOHAWK_LIVINGBOOKS_ITEMSCRIPT_H
+
+#include "common/scummsys.h"
+#include "common/array.h"
+#include "common/str.h"
+
+namespace Mohawk {
+
+struct LBKey {
+	byte code;
+	byte modifiers;
+	byte char_;
+	byte repeats;
+};
+
+struct LBScriptEntry {
+	LBScriptEntry();
+	~LBScriptEntry();
+
+	uint16 state;
+
+	uint16 type;
+	uint16 event;
+	uint16 opcode;
+	uint16 param;
+
+	uint16 argc;
+	uint16 *argvParam;
+	uint16 *argvTarget;
+
+	uint16 targetingType;
+	Common::Array<Common::String> targets;
+
+	// kLBNotifyChangeMode
+	uint16 newUnknown;
+	uint16 newMode;
+	uint16 newPage;
+	uint16 newSubpage;
+	Common::String newCursor;
+
+	// kLBEventNotified
+	uint16 matchFrom;
+	uint16 matchNotify;
+
+	// kLBOpSendExpression
+	uint32 offset;
+	// kLBOpJumpUnlessExpression
+	uint16 target;
+
+	uint16 dataType;
+	uint16 dataLen;
+	byte *data;
+
+	Common::Array<Common::String> conditions;
+	Common::Array<LBScriptEntry *> subentries;
+};
+
+} // End of namespace Mohawk
+
+#endif
diff --git a/engines/mohawk/livingbooks_itemtypes.h b/engines/mohawk/livingbooks_itemtypes.h
new file mode 100644
index 00000000000..2d1cc52aa31
--- /dev/null
+++ b/engines/mohawk/livingbooks_itemtypes.h
@@ -0,0 +1,205 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MOHAWK_LIVINGBOOKS_ITEMTYPES_H
+#define MOHAWK_LIVINGBOOKS_ITEMTYPES_H
+
+#include "mohawk/livingbooks_item.h"
+
+#include "common/array.h"
+#include "common/rect.h"
+
+namespace Common {
+	class MemoryReadStreamEndian;
+}
+
+namespace Mohawk {
+
+class MohawkEngine_LivingBooks;
+class LBPage;
+
+class LBSoundItem : public LBItem {
+public:
+	LBSoundItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
+	~LBSoundItem() override;
+
+	void update() override;
+	bool togglePlaying(bool playing, bool restart) override;
+	void stop() override;
+
+protected:
+	LBItem *createClone() override;
+
+	bool _running;
+};
+
+struct GroupEntry {
+	uint entryId;
+	uint entryType;
+};
+
+class LBGroupItem : public LBItem {
+public:
+	LBGroupItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
+
+	void readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) override;
+
+	void destroySelf() override;
+	void setEnabled(bool enabled) override;
+	void setGlobalEnabled(bool enabled) override;
+	bool contains(Common::Point point) override;
+	bool togglePlaying(bool playing, bool restart) override;
+	// 0x12
+	void seek(uint16 pos) override;
+	void setVisible(bool visible) override;
+	void setGlobalVisible(bool visible) override;
+	void startPhase(uint phase) override;
+	void stop() override;
+	void load() override;
+	void unload() override;
+	void moveBy(const Common::Point &pos) override;
+	void moveTo(const Common::Point &pos) override;
+
+protected:
+	LBItem *createClone() override;
+
+	bool _starting;
+
+	Common::Array<GroupEntry> _groupEntries;
+};
+
+class LBPaletteItem : public LBItem {
+public:
+	LBPaletteItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
+	~LBPaletteItem() override;
+
+	void readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) override;
+
+	bool togglePlaying(bool playing, bool restart) override;
+	void update() override;
+
+protected:
+	LBItem *createClone() override;
+
+	uint16 _fadeInPeriod, _fadeInStep, _drawStart, _drawCount;
+	uint32 _fadeInStart, _fadeInCurrent;
+	byte *_palette;
+};
+
+struct LiveTextWord {
+	Common::Rect bounds;
+	uint16 soundId;
+
+	uint16 itemType;
+	uint16 itemId;
+};
+
+struct LiveTextPhrase {
+	uint16 wordStart, wordCount;
+	uint16 highlightStart, highlightEnd;
+	uint16 startId, endId;
+};
+
+class LBLiveTextItem : public LBItem {
+public:
+	LBLiveTextItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
+
+	void readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) override;
+
+	bool contains(Common::Point point) override;
+	void update() override;
+	void draw() override;
+	void handleMouseDown(Common::Point pos) override;
+	bool togglePlaying(bool playing, bool restart) override;
+	void stop() override;
+	void notify(uint16 data, uint16 from) override;
+
+protected:
+	LBItem *createClone() override;
+
+	void paletteUpdate(uint16 word, bool on);
+	void drawWord(uint word, uint yPos);
+
+	uint16 _currentPhrase, _currentWord;
+
+	byte _backgroundColor[4];
+	byte _foregroundColor[4];
+	byte _highlightColor[4];
+	uint16 _paletteIndex;
+
+	Common::Array<LiveTextWord> _words;
+	Common::Array<LiveTextPhrase> _phrases;
+};
+
+class LBPictureItem : public LBItem {
+public:
+	LBPictureItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
+
+	void readData(uint16 type, uint16 size, Common::MemoryReadStreamEndian *stream) override;
+
+	bool contains(Common::Point point) override;
+	void draw() override;
+	void init() override;
+
+protected:
+	LBItem *createClone() override;
+};
+
+class LBMovieItem : public LBItem {
+public:
+	LBMovieItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
+	~LBMovieItem() override;
+
+	void update() override;
+	bool togglePlaying(bool playing, bool restart) override;
+
+protected:
+	LBItem *createClone() override;
+};
+
+class LBMiniGameItem : public LBItem {
+public:
+	LBMiniGameItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
+	~LBMiniGameItem() override;
+
+	bool togglePlaying(bool playing, bool restart) override;
+
+protected:
+	LBItem *createClone() override;
+};
+
+class LBProxyItem : public LBItem {
+public:
+	LBProxyItem(MohawkEngine_LivingBooks *_vm, LBPage *page, Common::Rect rect);
+	~LBProxyItem() override;
+
+	void load() override;
+	void unload() override;
+
+protected:
+	LBItem *createClone() override;
+
+	class LBPage *_page;
+};
+
+} // End of namespace Mohawk
+
+#endif
diff --git a/engines/mohawk/livingbooks_page.h b/engines/mohawk/livingbooks_page.h
new file mode 100644
index 00000000000..02e095964b7
--- /dev/null
+++ b/engines/mohawk/livingbooks_page.h
@@ -0,0 +1,61 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MOHAWK_LIVINGBOOKS_PAGE_H
+#define MOHAWK_LIVINGBOOKS_PAGE_H
+
+#include "common/array.h"
+
+namespace Mohawk {
+
+class MohawkEngine_LivingBooks;
+class LBItem;
+class LBCode;
+class Archive;
+
+class LBPage {
+public:
+	LBPage(MohawkEngine_LivingBooks *vm);
+	~LBPage();
+
+	void open(Archive *mhk, uint16 baseId);
+	uint16 getResourceVersion();
+
+	void addClonedItem(LBItem *item);
+	void itemDestroyed(LBItem *item);
+
+	LBCode *_code;
+
+protected:
+	MohawkEngine_LivingBooks *_vm;
+
+	Archive *_mhk;
+	Common::Array<LBItem *> _items;
+
+	uint16 _baseId;
+	bool _cascade;
+
+	void loadBITL(uint16 resourceId);
+};
+
+} // End of namespace Mohawk
+
+#endif
diff --git a/engines/mohawk/module.mk b/engines/mohawk/module.mk
index b264987cae7..c7119b0f973 100644
--- a/engines/mohawk/module.mk
+++ b/engines/mohawk/module.mk
@@ -12,6 +12,8 @@ MODULE_OBJS = \
 	livingbooks_bookinfo.o \
 	livingbooks_code.o \
 	livingbooks_graphics.o \
+	livingbooks_item.o \
+	livingbooks_itemscript.o \
 	livingbooks_itemtypes.o \
 	livingbooks_lbx.o \
 	livingbooks_menus.o \




More information about the Scummvm-git-logs mailing list