/* 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/>.
 *
 */

/*
 * This code is based on original Sfinx source code
 * Copyright (c) 1994-1997 Janusz B. Wisniewski and L.K. Avalon
 */

#include "sound.h"
#include "cge2/cge2_main.h"
#include "cge2/cge2.h"
#include "cge2/vga13h.h"
#include "cge2/text.h"
#include "cge2/snail.h"
#include "cge2/hero.h"
#include "cge2/spare.h"
#include "cge2/map.h"

namespace CGE2 {

System::System(CGE2Engine *vm) : Sprite(vm), _vm(vm) {
	_blinkCounter = 0;
	_blinkSprite = nullptr;
	tick();
}

void System::touch(uint16 mask, V2D pos) {
	if (mask & kEventEsc) {
		// The original was calling keyClick()
		// The sound is uselessly annoying and noisy, so it has been removed
		_vm->killText();
		if (_vm->_gamePhase == kPhaseIntro) {
			_vm->_commandHandler->addCommand(kCmdClear, -1, 0, nullptr);
			return;
		}
	} else {
		if (_vm->_gamePhase != kPhaseInGame)
			return;
		_vm->_infoLine->setText(nullptr);

		if (mask & kMouseLeftUp) {
			if (pos.y >= 0)	{ // world
				if (!_vm->_talk && pos.y < _vm->_mouseTop)
					_vm->_heroTab[_vm->_sex]->_ptr->walkTo(pos);
			} else { // panel
				if (_vm->_commandHandler->idle()) {
					int sex = pos.x < kPocketsWidth;
					if (sex || pos.x >= kScrWidth - kPocketsWidth) {
						_vm->switchHero(sex);
						if (_vm->_sex == sex) {
							int dx = kPocketsWidth >> 1,
								dy = 1 - (kPanHeight >> 1);
							Sprite *s;
							if (!sex)
								pos.x -= kScrWidth - kPocketsWidth;
							dx -= pos.x;
							dy -= pos.y;
							if (dx * dx + dy * dy > 10 * 10) {
								int n = 0;
								if (1 - pos.y >= (kPanHeight >> 1))
									n += 2;
								if (pos.x >= (kPocketsWidth >> 1))
									++n;
								s = _vm->_heroTab[_vm->_sex]->_pocket[n];
								if (_vm->_sys->_blinkSprite)
									_vm->_sys->_blinkSprite->_flags._hide = false;
								if (_vm->_sys->_blinkSprite == s)
									_vm->_sys->_blinkSprite = nullptr;
								else
									_vm->_sys->_blinkSprite = s;
							}
						}
					}
				}
			}
		}
	}
}

void System::tick() {
	_time = kSysTimeRate;

	if (_blinkCounter)
		--_blinkCounter;
	else {
		if (_blinkSprite)
			_blinkSprite->_flags._hide ^= 1;
		_blinkCounter = kBlinkRate;
	}
}

int CGE2Engine::number(char *str) {
	char *s = token(str);
	if (s == nullptr)
		error("Wrong input for CGE2Engine::number()");
	int r = atoi(s);
	char *pp = strchr(s, ':');
	if (pp)
		r = (r << 8) + atoi(pp + 1);
	return r;
}

char *CGE2Engine::token(char *s) {
	return strtok(s, " =\t,;/()");
}

char *CGE2Engine::tail(char *s) {
	if (s && (*s == '='))
		s++;
	return s;
}

int CGE2Engine::takeEnum(const char *const *tab, const char *text) {
	if (text) {
		for (const char *const *e = tab; *e; e++) {
			if (scumm_stricmp(text, *e) == 0)
				return e - tab;
		}
	}
	return -1;
}

ID CGE2Engine::ident(const char *s) {
	return ID(takeEnum(EncryptedStream::kIdTab, s));
}

bool CGE2Engine::testBool(char *s) {
	return number(s) != 0;
}

void CGE2Engine::badLab(const char *fn) {
	error("Misplaced label in %s!", fn);
}

Sprite *CGE2Engine::loadSprite(const char *fname, int ref, int scene, V3D &pos) {
	int shpcnt = 0;
	int seqcnt = 0;
	int cnt[kActions];

	for (int i = 0; i < kActions; i++)
		cnt[i] = 0;

	ID section = kIdPhase;
	bool frnt = true;
	bool east = false;
	bool port = false;
	bool tran = false;
	Hero *h;
	ID id;

	char tmpStr[kLineMax + 1];
	static_assert(sizeof(tmpStr) >= kPathMax, "mergeExt expects kPathMax buffer");
	mergeExt(tmpStr, fname, kSprExt);

	if (_resman->exist(tmpStr)) { // sprite description file exist
		EncryptedStream sprf(_resman, tmpStr);
		if (sprf.err())
			error("Bad SPR [%s]", tmpStr);

		int label = kNoByte;
		Common::String line;

		for (line = sprf.readLine(); !sprf.eos(); line = sprf.readLine()){
			if (line.empty())
				continue;
			Common::strlcpy(tmpStr, line.c_str(), sizeof(tmpStr));

			char *p = token(tmpStr);
			if (*p == '@') {
				if (label != kNoByte)
					badLab(fname);
				label = atoi(p + 1);
				continue;
			}

			id = ident(p);
			switch (id) {
			case kIdName: // will be taken in Expand routine
				if (label != kNoByte)
					badLab(fname);
				break;
			case kIdType:
				if (label != kNoByte)
					badLab(fname);
				break;
			case kIdNear:
			case kIdMTake:
			case kIdFTake:
			case kIdPhase:
			case kIdSeq:
				if (label != kNoByte)
					badLab(fname);
				section = id;
				break;
			case kIdFront:
				if (label != kNoByte)
					badLab(fname);
				p = token(nullptr);
				frnt = testBool(p);
				break;
			case kIdEast:
				if (label != kNoByte)
					badLab(fname);
				p = token(nullptr);
				east = testBool(p);
				break;
			case kIdPortable:
				if (label != kNoByte)
					badLab(fname);
				p = token(nullptr);
				port = testBool(p);
				break;
			case kIdTransparent:
				if (label != kNoByte)
					badLab(fname);
				p = token(nullptr);
				tran = testBool(p);
				break;
			default:
				if (id >= kIdNear)
					break;
				switch (section) {
				case kIdNear:
				case kIdMTake:
				case kIdFTake:
					if (_commandHandler->getComId(p) >= 0)
						++cnt[section];
					else
						error("Bad line %d [%s]", sprf.getLineCount(), tmpStr);
					break;
				case kIdPhase:
					if (label != kNoByte)
						badLab(fname);
					++shpcnt;
					break;
				case kIdSeq:
					if (label != kNoByte)
						badLab(fname);
					++seqcnt;
					break;
				default:
					break;
				}
				break;
			}
			label = kNoByte;
		}

		if (!shpcnt)
			error("No shapes - %s", fname);
	} else // No sprite description: mono-shaped sprite with only .BMP file.
		++shpcnt;

	// Make sprite of chosen type:
	Sprite *sprite = nullptr;
	char c = *fname | 0x20;
	if (c >= 'a' && c <= 'z' && fname[1] == '0' && fname[2] == '\0') {
		h = new Hero(this);
		h->gotoxyz(pos);
		sprite = h;
	} else {
		sprite = new Sprite(this);
		sprite->gotoxyz(pos);
	}

	if (sprite) {
		sprite->_ref = ref;
		sprite->_scene = scene;

		sprite->_flags._frnt = frnt;
		sprite->_flags._east = east;
		sprite->_flags._port = port;
		sprite->_flags._tran = tran;
		sprite->_flags._kill = true;

		// Extract the filename, without the extension
		Common::strlcpy(sprite->_file, fname, sizeof(sprite->_file));
		char *p = strchr(sprite->_file, '.');
		if (p)
			*p = '\0';

		sprite->_shpCnt = shpcnt;
		sprite->_seqCnt = seqcnt;

		for (int i = 0; i < kActions; i++)
			sprite->_actionCtrl[i]._cnt = cnt[i];
	}

	return sprite;
}

void CGE2Engine::loadScript(const char *fname, bool onlyToolbar) {
	EncryptedStream scrf(_resman, fname);

	if (scrf.err())
		return;

	bool ok = true;

	char tmpStr[kLineMax + 1];
	Common::String line;

	for (line = scrf.readLine(); !scrf.eos(); line = scrf.readLine()) {
		if (line.empty())
			continue;

		Common::strlcpy(tmpStr, line.c_str(), sizeof(tmpStr));

		ok = false; // not OK if break

		V3D P;

		// sprite ident number
		int SpI = number(tmpStr);

		if (onlyToolbar && SpI >= 141)
			return;

		// sprite file name
		char *SpN;
		if ((SpN = token(nullptr)) == nullptr)
			break;

		// sprite scene
		int SpA = number(nullptr);

		// sprite column
		P._x = number(nullptr);

		// sprite row
		P._y = number(nullptr);

		// sprite Z pos
		P._z = number(nullptr);

		// sprite life
		bool BkG = number(nullptr) == 0;

		ok = true; // no break: OK

		Sprite *sprite = loadSprite(SpN, SpI, SpA, P);
		if (sprite) {
			if (BkG)
				sprite->_flags._back = true;

			int n = _spare->count();
			if (_spare->locate(sprite->_ref) == nullptr)
				_spare->dispose(sprite);
			else
				delete sprite;

			if (_spare->count() == n)
				error("Duplicate reference! %s", SpN);
		}
	}

	if (!ok)
		error("Bad INI line %d [%s]", scrf.getLineCount(), fname);
}

void CGE2Engine::movie(const char *ext) {
	assert(ext);

	if (_quitFlag)
		return;

	char fn[12];
	snprintf(fn, 12, "CGE%s", ext);

	if (_resman->exist(fn)) {
		int now = _now;
		_now = atoi(ext + 2);
		loadScript(fn);
		sceneUp(_now);
		_keyboard->setClient(_sys);

		while (!_commandHandler->idle() && !_quitFlag)
			mainLoop();

		_keyboard->setClient(nullptr);
		_commandHandler->addCommand(kCmdClear, -1, 0, nullptr);
		_commandHandlerTurbo->addCommand(kCmdClear, -1, 0, nullptr);
		_spare->clear();
		_vga->_showQ->clear();
		_now = now;
	}
}

void CGE2Engine::sceneUp(int cav) {
	_now = cav;
	int bakRef = _now << 8;
	if (_music)
		_midiPlayer->loadMidi(bakRef);
	showBak(bakRef);
	*_eye = *(_eyeTab[_now]);
	_mouseTop = V2D(this, V3D(0, 1, kScrDepth)).y;
	_map->load(_now);
	_spare->takeScene(_now);
	openPocket();

	for (int i = 0; i < 2; i++) {
		Hero *h = _heroTab[i]->_ptr;
		if (h && h->_scene == _now) {
			V2D p = *_heroTab[i]->_posTab[_now];
			h->gotoxyz(V3D(p.x, 0, p.y));
			h->clrHide();
			_vga->_showQ->insert(h);
			h->park();
			h->setCurrent();
			h->setContact();
		}
	}

	_sound->stop();
	_fx->clear();

	selectPocket(-1);
	_infoLine->setText(nullptr);
	busy(false);

	if (!_dark)
		_vga->sunset();

	_vga->show();
	_vga->copyPage(1, 0);
	_vga->show();
	_vga->sunrise(_vga->_sysPal);

	_dark = false;

	if (_gamePhase == kPhaseInGame)
		_mouse->on();

	feedSnail(_vga->_showQ->locate(bakRef + 255), kNear, _heroTab[_sex]->_ptr);
}

void CGE2Engine::sceneDown() {
	busy(true);
	_soundStat._wait = nullptr; // unlock snail
	Sprite *spr = _vga->_showQ->locate((_now << 8) | 254);

	if (spr)
		feedSnail(spr, kNear, _heroTab[_sex]->_ptr);

	while (!(_commandHandler->idle() && _commandHandlerTurbo->idle())) {
		_commandHandlerTurbo->runCommand();
		_commandHandler->runCommand();
	}

	closePocket();
	for (int i = 0; i < 2; i++)
		_spare->update(_vga->_showQ->remove(_heroTab[i]->_ptr));
	_spare->dispose();
}

void CGE2Engine::switchScene(int scene) {
	if (scene == _now)
		return;

	_req = scene;
	storeHeroPos();
	*(_eyeTab[_now]) = *_eye;

	if (scene < 0)
		_commandHandler->addCallback(kCmdExec, -1, 0, kQGame); // quit game
	else {
		if (_heroTab[_sex]->_ptr->_scene == _now) {
			_heroTab[_sex]->_ptr->setScene(scene);
			if (_heroTab[!_sex]->_ptr->_scene == _now)
				_heroTab[!_sex]->_ptr->setScene(scene);
		}
		_mouse->off();
		if (_heroTab[_sex]->_ptr)
			_heroTab[_sex]->_ptr->park();
		killText();
		_commandHandler->addCallback(kCmdExec, -1, 0, kXScene); // switch scene
	}
}

void CGE2Engine::storeHeroPos() {
	for (int i = 0; i < 2; i++) {
		Hero *h = _heroTab[i]->_ptr;
		if (h->_scene == _now) {
			delete _heroTab[i]->_posTab[_now];
			V2D *temp = new V2D(this, h->_pos3D._x.trunc(), h->_pos3D._z.trunc());
			_heroTab[i]->_posTab[_now] = temp;
		}
	}
}

void CGE2Engine::showBak(int ref) {
	Sprite *spr = _spare->locate(ref);
	if (spr != nullptr) {
		_bitmapPalette = _vga->_sysPal;
		spr->expand();
		_bitmapPalette = nullptr;
		spr->show(2);
		_vga->copyPage(1, 2);
	}
}

void CGE2Engine::mainLoop() {
	if (_gamePhase == kPhaseInGame)
		checkSounds();

	_vga->show();
	_commandHandlerTurbo->runCommand();
	_commandHandler->runCommand();

	// Handle a delay between game frames
	handleFrame();

	// Handle any pending events
	_eventManager->poll();

	// Check shouldQuit()
	_quitFlag = shouldQuit();
}

void CGE2Engine::checkSounds() {
	checkMute();
	_sound->checkSoundHandles();
	checkVolumeSwitches();
	_midiPlayer->syncVolume();
	syncSoundSettings();
}

void CGE2Engine::handleFrame() {
	// Game frame delay
	uint32 millis = g_system->getMillis();
	while (!_quitFlag && (millis < (_lastFrame + kGameFrameDelay))) {
		// Handle any pending events
		_eventManager->poll();

		if (millis >= (_lastTick + kGameTickDelay)) {
			// Dispatch the tick to any active objects
			tick();
			_lastTick = millis;
		}

		// Slight delay
		g_system->delayMillis(5);
		millis = g_system->getMillis();
	}
	_lastFrame = millis;

	if (millis >= (_lastTick + kGameTickDelay)) {
		// Dispatch the tick to any active objects
		tick();
		_lastTick = millis;
	}
}

Sprite *CGE2Engine::locate(int ref) {
	_taken = false;
	Sprite *spr = _vga->_showQ->locate(ref);
	if (!spr) {
		spr = _spare->locate(ref);
		if (spr)
			_taken = true;
	}
	return spr;
}

bool CGE2Engine::isHero(Sprite *spr) {
	return spr && spr->_ref / 10 == 14;
}

void CGE2Engine::tick() {
	// system pseudo-sprite
	if (_sys && _sys->_time && (--_sys->_time == 0))
		_sys->tick();

	for (Sprite *spr = _vga->_showQ->first(); spr; spr = spr->_next) {
		if (spr->_time && (--spr->_time == 0))
			spr->tick();

		if (_waitRef && (_waitRef == spr->_ref) && spr->seqTest(_waitSeq))
			_waitRef = 0;
	}

	_mouse->tick();
}

void CGE2Engine::busy(bool on) {
	if (on) {
		_spriteNotify = _midiNotify = &CGE2::CGE2Engine::busyStep;
		busyStep();
	} else {
		if (_busyPtr)
			_busyPtr->step(0);
		_spriteNotify = _midiNotify = nullptr;
	}
}

void CGE2Engine::busyStep() {
	if (_busyPtr) {
		_busyPtr->step((_busyPtr->_seqPtr) ? -1 : 1);
		_busyPtr->show(0);
	}
}

void CGE2Engine::runGame() {
	if (_quitFlag)
		return;

	loadUser();
	sceneUp(_now);
	initToolbar();

	// main loop
	while (!_endGame && !_quitFlag)
		mainLoop();

	// If leaving the game (close window, return to launcher, etc.
	// - except finishing the game), explicitly save it's state:
	if (!_endGame && canSaveGameStateCurrently())
		qGame();

	_keyboard->setClient(nullptr);
	_commandHandler->addCommand(kCmdClear, -1, 0, nullptr);
	_commandHandlerTurbo->addCommand(kCmdClear, -1, 0, nullptr);
	_mouse->off();
}

void CGE2Engine::loadUser() {
	loadPos();

	if (_startGameSlot != -1)
		loadGame(_startGameSlot);
	else {
		loadScript("CGE.INI");
		loadHeroes();
	}
}

void CGE2Engine::loadHeroes() { // Original name: loadGame()
	// load sprites & pocket

	Sprite *s;
	Hero *h = nullptr;

	// initialize Andzia/Anna
	s = _spare->take(142);
	if (s) {
		h = new Hero(this);
		*(Sprite*)h = *s;
		delete s;
		h->expand();
		_spare->update(h);
	}
	_heroTab[0]->_ptr = h;
	s = _spare->locate(152);
	_vga->_showQ->insert(s);
	_heroTab[0]->_face = s;

	// initialize Wacek/Vincent
	s = _spare->take(141);
	if (s) {
		h = new Hero(this);
		*(Sprite*)h = *s;
		delete s;
		h->expand();
		_spare->update(h);
	}
	_heroTab[1]->_ptr = h;
	s = _spare->locate(151);
	_vga->_showQ->insert(s);
	_heroTab[1]->_face = s;

	//--- start!
	switchHero(_sex);
}

void CGE2Engine::loadPos() {
	if (_resman->exist("CGE.HXY")) {
		for (int cav = 0; cav < kSceneMax; cav++)
			_heroTab[1]->_posTab[cav] = new V2D(this, 180, 10);

		EncryptedStream file(_resman, "CGE.HXY");

		for (int cav = 0; cav < kSceneMax; cav++) {
			_heroTab[0]->_posTab[cav] = new V2D(this);
			_heroTab[0]->_posTab[cav]->x = file.readSint16LE();
			_heroTab[0]->_posTab[cav]->y = file.readSint16LE();
		}

		for (int cav = 0; cav < 41; cav++) { // (564 - 400) / 4 = 41
			_heroTab[1]->_posTab[cav]->x = file.readSint16LE();
			_heroTab[1]->_posTab[cav]->y = file.readSint16LE();
		}
	} else
		error("Missing file: CGE.HXY");
}

void CGE2Engine::loadTab() {
	setEye(_text->getText(240));
	for (int i = 0; i < kSceneMax; i++)
		*(_eyeTab[i]) = *_eye;

	if  (_resman->exist(kTabName)) {
		EncryptedStream f(_resman, kTabName);

		for (int i = 0; i < kSceneMax; i++) {
			uint32 v = f.readUint32LE();
			_eyeTab[i]->_x = FXP(v >> 8, static_cast<int>((int8)(v & 0xff)));

			v = f.readUint32LE();
			_eyeTab[i]->_y = FXP(v >> 8, static_cast<int>((int8)(v & 0xff)));

			v = f.readUint32LE();
			_eyeTab[i]->_z = FXP(v >> 8, static_cast<int>((int8)(v & 0xff)));
		}
	}
}

void CGE2Engine::cge2_main() {
	loadTab();

	if (_startGameSlot != -1) {
		// Starting up a savegame from the launcher
		runGame();
		return;
	}

	if (showTitle("WELCOME")) {
		movie(kIntroExt);

		if (_text->getText(255) != nullptr) {
			runGame();
			_gamePhase = kPhaseOver;
		}

		_vga->sunset();
	} else
		_vga->sunset();
}

char *CGE2Engine::mergeExt(char *buf, const char *name, const char *ext) {
	Common::strcpy_s(buf, kPathMax, name);
	char *dot = strrchr(buf, '.');
	if (!dot)
		Common::strcat_s(buf, kPathMax, ext);

	return buf;
}

void CGE2Engine::setEye(const V3D &e) {
	*_eye = e;
}

void CGE2Engine::setEye(const V2D& e2, int z) {
	_eye->_x = e2.x;
	_eye->_y = e2.y;
	_eye->_z = z;
}

void CGE2Engine::setEye(const char *s) {
	size_t ln = strlen(s) + 1;
	char *tempStr = new char[ln];
	Common::strcpy_s(tempStr, ln, s);
	_eye->_x = atoi(token(tempStr));
	_eye->_y = atoi(token(nullptr));
	_eye->_z = atoi(token(nullptr));
	delete[] tempStr;
}

int CGE2Engine::newRandom(int range) {
	if (!range)
		return 0;

	return _randomSource.getRandomNumber(range - 1);
}

bool CGE2Engine::showTitle(const char *name) {
	if (_quitFlag)
		return false;

	_bitmapPalette = _vga->_sysPal;
	BitmapPtr LB = new Bitmap[1];
	LB[0] = Bitmap(this, name);
	_bitmapPalette = nullptr;

	Sprite D(this, LB, 1);
	D._flags._kill = true;
	D.gotoxyz(kScrWidth >> 1, -(kPanHeight >> 1));

	_vga->sunset();
	D.show(2);
	_vga->copyPage(1, 2);
	_vga->copyPage(0, 1);
	_vga->sunrise(_vga->_sysPal);
	_vga->update();

	g_system->delayMillis(2500);

	return true;
}

void CGE2Engine::killText() {
	if (!_talk)
		return;

	_commandHandlerTurbo->addCommand(kCmdKill, -1, 0, _talk);
	_talk = nullptr;
}

void CGE2Engine::switchHero(int sex) {
	if (sex != _sex) {
		int scene = _heroTab[sex]->_ptr->_scene;
		if (_sys->_blinkSprite) {
			_sys->_blinkSprite->_flags._hide = false;
			_sys->_blinkSprite = nullptr;
		}

		if (scene >= 0) {
			_commandHandler->addCommand(kCmdSeq, -1, 2, _heroTab[_sex]->_face);
			_sex ^= 1;
			switchScene(scene);
		}
	}
	Sprite *face = _heroTab[_sex]->_face;
	if (face->_seqPtr == 0)
		_commandHandler->addCommand(kCmdSeq, -1, 1, face);
}

void Sprite::touch(uint16 mask, V2D pos) {
	if ((mask & kEventAttn) != 0)
		return;

	if (_vm->_gamePhase == kPhaseInGame)
		_vm->_infoLine->setText(name());

	if (_ref < 0)
		return; // cannot access system sprites

	if (_ref / 10 == 12) {
		_vm->optionTouch(_ref % 10, mask);
		return;
	}

	if ((mask & kMouseLeftUp) && _vm->_commandHandler->idle()) {
		if (_vm->isHero(this) && !_vm->_sys->_blinkSprite) {
			_vm->switchHero((this == _vm->_heroTab[1]->_ptr) ? 1 : 0);
		} else if (_flags._kept) { // sprite in pocket
			for (int sex = 0; sex < 2; ++sex) {
				for (int p = 0; p < kPocketMax; ++p) {
					if (_vm->_heroTab[sex]->_pocket[p] == this) {
						_vm->switchHero(sex);
						if (_vm->_sex == sex) {
							if (_vm->_sys->_blinkSprite)
								_vm->_sys->_blinkSprite->_flags._hide = false;

							if (_vm->_sys->_blinkSprite == this)
								_vm->_sys->_blinkSprite = nullptr;
							else
								_vm->_sys->_blinkSprite = this;
						}
					}
				}
			}
		} else { // sprite NOT in pocket
			Hero *h = _vm->_heroTab[_vm->_sex]->_ptr;
			if (!_vm->_talk) {
				// the "+3" is a hack used to work around a script issue in scene 5
				if ((_ref & 0xFF) < 200 && h->distance(this) > (h->_maxDist << 1) + 3)
					h->walkTo(this);
				else if (_vm->_sys->_blinkSprite) {
					if (works(_vm->_sys->_blinkSprite)) {
						_vm->feedSnail(_vm->_sys->_blinkSprite, (_vm->_sex) ? kMTake : kFTake, _vm->_heroTab[_vm->_sex]->_ptr);
						_vm->_sys->_blinkSprite->_flags._hide = false;
						_vm->_sys->_blinkSprite = nullptr;
					} else
						_vm->offUse();

					_vm->selectPocket(-1);
					// else, no pocket sprite selected
				} else if (_flags._port) { // portable
					if (_vm->findActivePocket(-1) < 0)
						_vm->pocFul();
					else {
						_vm->_commandHandler->addCommand(kCmdReach, -2, _ref, nullptr);
						_vm->_commandHandler->addCommand(kCmdKeep, -1, -1, this);
						_flags._port = false;
					}
				} else { // non-portable
					Action a = h->action();
					if (_actionCtrl[a]._cnt) {
						CommandHandler::Command *cmdList = snList(a);
						if (cmdList[_actionCtrl[a]._ptr]._commandType == kCmdNext)
							_vm->offUse();
						else
							_vm->feedSnail(this, a, h);
					} else
						_vm->offUse();
				}
			}
		}
	}
}

void CGE2Engine::keyClick() {
	_commandHandlerTurbo->addCommand(kCmdSound, -1, 5, nullptr);
}

void CGE2Engine::offUse() {
	int seq = 0;
	int offUseCount = atoi(_text->getText(kOffUseCount));

	// This fixes the issue of empty speech bubbles in the original.
	// Now we only let this cycle pass if it randoms a valid value for getText().
	int txt = 0;
	do {
		txt = kOffUseText + _sex * offUseCount + newRandom(offUseCount);
	} while (_text->getText(txt) == nullptr);

	Hero *h = _heroTab[_sex]->_ptr;
	h->park();
	_commandHandler->addCommand(kCmdWait, -1, -1, h);
	_commandHandler->addCommand(kCmdSeq, -1, seq, h);
	if (!_sayVox)
		_commandHandler->addCommand(kCmdSound, -1, 6 + _sex, h);
	_commandHandler->addCommand(kCmdWait, -1, -1, h);
	_commandHandler->addCommand(kCmdSay, -1, txt, h);
}

Sprite *CGE2Engine::spriteAt(V2D pos) {
	Sprite *spr;

	for (spr = _vga->_showQ->last(); spr; spr = spr->_prev) {
		if (!spr->_flags._hide && !spr->_flags._tran && (spr->getShp()->solidAt(pos - spr->_pos2D)))
			break;
	}

	return spr;
}

} // End of namespace CGE2
