Files
2026-02-02 04:50:13 +01:00

577 lines
15 KiB
C++

/* 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/>.
*
* Copyright 2020 Google
*
*/
#include "hadesch/hadesch.h"
#include "hadesch/video.h"
#include "hadesch/ambient.h"
namespace Hadesch {
static const char *kHighlightImage = "r6010ol0";
static const char *kMaterialsImage = "r6010ok0";
static const char *kMaterialsMoveImage = "r6020ba0";
static const char *kMinotaurImage = "r6040ba0";
static const char *kDigits = "0123456789";
static const int numSquares = 25;
static const char *minotaurStates[] = {
"",
"",
"",
"inactive",
"encourage",
"trapped",
"escaped",
"critical",
"level",
"idle"
};
enum DaedalusDialogState {
kMinotaur0 = 0,
kMinotaur1 = 1,
kMinotaur2 = 2,
kMinotaurInactive = 3,
kMinotaurEncourage = 4,
kMinotaurTrapped = 5,
kMinotaurEscaped = 6,
kMinotaurCritical = 7,
kMinotaurLevel = 8,
kMinotaurIdle = 9
};
enum Strength {
kStrengthStraw = 1,
kStrengthWood = 2,
kStrengthBricks = 3,
kStrengthStone = 4
};
enum Position {
kDown = 0,
kLeft = 1,
kUp = 2,
kRight = 3
};
static const char *dirnames[] = {
"down",
"left",
"up",
"right"
};
enum {
kRerenderLabyrinth = 1017001
};
static const char *daedalusSoundSMK[] = {
"R6100nA0",
"R6100wA0",
"R6100nB0",
"R6100wC0",
"R6100nD0",
};
static const TranscribedSound daedalusSoundsAIF[] = {
{"R6100nH0", _hs("Help us to move the walls so that they are strong enough to stop the minotaur")},
{"R6100nL0", _hs("Click on a square to rotate the walls")},
{"R6100nG0", _hs("Some walls are already locked in place and won't rotate")},
{"R6100nK0", _hs("If you need help, refer to workman's equations")},
{"R6170nA0", _hs("Careful, my friend. Some of the walls are not strong enough")},
{"R6150nA0", _hs("You're a brave bullfighter, my friend")},
{"R6150nB0", _hs("Keep it up. It looks like he's tiring")},
{"R6150nC0", _hs("That's taking the bull by the horns")},
{"R6150nD0", _hs("Don't give up. You can't beat him")},
{"R6180nA0", _hs("You have beaten the Minotaur. You have the makings of a hero")},
{"R6180nC0", _hs("You have beaten the beast at last")},
{"R6180nD0", _hs("You have done it. The people of Crete are once again safe")},
{"R6170nC0", _hs("Let's try again")},
{"R6170nD0", _hs("Warn the people of Crete: the Minotaur has escaped. Workers, keep the Minotaur back in the labyrinth")},
{"R6170nE0", _hs("I believe you and the Minotaur have not seen the last of one another")},
{"R6170nF0", _hs("Ah that was a nobble effort, my friend")},
{"R6160nA0", _hs("The Minotaur has broken though a critical wall. Workers, calm on the beast")},
{"R6090eA0", _hs("Eh. Hm")},
{"R6190nA0", _hs("Ok. Onto level two")},
{"R6190nB0", _hs("Onto level three")},
};
struct Wall {
int _id;
bool _isCritical;
int _inTransit;
Strength _strength;
Position _position;
Wall() {
_id = -1;
_isCritical = false;
_strength = kStrengthStraw;
_position = kDown;
_inTransit = 0;
}
};
struct Cell {
Common::Array<Wall> _movableWalls;
Common::Array<Wall> _immovableWalls;
bool _isRotatable;
Cell() {
_isRotatable = false;
}
};
struct Labyrinth {
Cell _cells[25];
};
class MinotaurHandler : public Handler {
public:
MinotaurHandler() {
_highlight = -1;
_dialogState = kMinotaur0;
_minotaurX = 1;
_minotaurY = 2;
_minotaurTileId = 7;
_soundCounter = 0;
_soundMax = 5;
_lastChargeSound = -1;
_lastTrappedSound = -1;
_lastEscapedSound = -1;
_levelId = 0;
// consts
xVector = Common::Point(-55, -33);
yVector = Common::Point(+55, -33);
}
void handleClick(const Common::String &name) override {
if (name.firstChar() >= '0' && name.firstChar() <= '9') {
rotate(name.asUint64());
renderLabyrinth();
return;
}
/* TODO: MNSH: Daedalus */
}
void handleEvent(int eventId) override {
Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
switch (eventId) {
case kRerenderLabyrinth:
renderLabyrinth();
break;
case 17953:
g_vm->addTimer(17954, 300);
break;
case 17954:
treatDialogState();
break;
}
}
void handleMouseOver(const Common::String &name) override {
if (name.firstChar() >= '0' && name.firstChar() <= '9')
_highlight = name.asUint64();
else
_highlight = -1;
renderLabyrinth();
}
void handleMouseOut(const Common::String &name) override {
_highlight = -1;
renderLabyrinth();
}
void prepareRoom() override {
Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
room->loadHotZones("minotaur.hot", true);
// TODO: load other puzzles
loadPuzzle("3x3j");
room->addStaticLayer("r6010pA0", 10000);
room->addStaticLayer("r6010tA0", 6400);
room->addStaticLayer("r6010oA0", 5500);
room->addStaticLayer("r6010oB0", 4000);
renderLabyrinth();
g_vm->getHeroBelt()->setColour(HeroBelt::kCool);
setDialogState(kMinotaur1);
}
bool handleCheat(const Common::String &cheat) override {
for (unsigned i = 0; i < ARRAYSIZE(minotaurStates); i++)
if (minotaurStates[i][0] && minotaurStates[i] == cheat) {
setDialogState(DaedalusDialogState(i));
return true;
}
return false;
}
private:
void setDialogState(DaedalusDialogState state) {
if (_dialogState) {
// TODO
} else {
_dialogState = state;
}
}
void playDaedalusSound(int index) {
// TODO: balance
_currentSound = index;
Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
if (index < ARRAYSIZE(daedalusSoundSMK))
room->playVideo(daedalusSoundSMK[index], 17953);
else
room->playSpeech(daedalusSoundsAIF[index-ARRAYSIZE(daedalusSoundSMK)], 17953);
}
void playDaedalusSoundWrap() {
Persistent *persistent = g_vm->getPersistent();
int index = 0;
switch (_dialogState) {
case kMinotaur0:
treatDialogState();
return;
case kMinotaur1:
index = _soundCounter;
break;
case kMinotaur2:
index = _soundCounter + 5;
break;
case kMinotaurInactive:
index = 9;
break;
case kMinotaurEncourage:
index = randomExcept(10, 13, _lastChargeSound);
_lastChargeSound = index;
break;
case kMinotaurTrapped:
if (persistent->_quest == kCreteQuest)
index = 14;
else
index = randomExcept(14, 16, _lastTrappedSound);
_lastTrappedSound = index;
break;
case kMinotaurEscaped:
index = randomExcept(17, 20, _lastEscapedSound);
_lastEscapedSound = index;
break;
case kMinotaurCritical:
index = 21;
break;
case kMinotaurLevel:
index = 23 + _levelId / 15;
break;
case kMinotaurIdle:
index = 22;
break;
}
playDaedalusSound(index);
}
int randomExcept(int from, int to, int except) {
Common::RandomSource &r = g_vm->getRnd();
if (except < from || except > to)
return r.getRandomNumberRng(from, to);
int x = r.getRandomNumberRng(from, to - 1);
if (x >= except)
x++;
return x;
}
void treatDialogState() {
Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
switch(_dialogState) {
case kMinotaur1:
if (_soundCounter < _soundMax) {
playDaedalusSoundWrap();
_soundCounter++;
return;
}
setDialogState(kMinotaur2);
return;
case kMinotaur2:
if (_soundCounter < _soundMax) {
playDaedalusSoundWrap();
_soundCounter++;
return;
}
room->enableMouse();
setDialogState(kMinotaur0);
return;
default:
// TODO: implement this;
return;
}
}
void renderLabyrinth() {
Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
if (_highlight >= 0)
room->selectFrame(kHighlightImage, 9990, 0,
getTileBase(_highlight)
- Common::Point(34, 52));
else
room->stopAnim(kHighlightImage);
for (int cell = 0; cell < numSquares; cell++) {
for (int j = 0; j < ARRAYSIZE(dirnames); j++) {
room->stopAnim(
LayerId(kMaterialsImage, cell, Common::String(dirnames[j]) + "outer"));
room->stopAnim(
LayerId(kMaterialsImage, cell, Common::String(dirnames[j]) + "inner"));
room->stopAnim(
LayerId(kMaterialsMoveImage, cell, "to-" + Common::String(dirnames[j])));
}
for (int j = 0; j < (int) _current._cells[cell]._movableWalls.size(); j++) {
renderWall(cell, _current._cells[cell]._movableWalls[j], false);
}
// Both original engine and us we're not able to handle 3
// walls in the same place. The only way to avoid this from every hapenning
// is to never put an immovable wall between 2 cells with movable walls.
// Hence if we have any movable walls then we can make immovable walls outer
// and they will not conflict with existing labyrinths.
// If we have no movable walls we can easily put all immovable walls as inner
bool immovableAreOuter = !_current._cells[cell]._movableWalls.empty();
for (int j = 0; j < (int) _current._cells[cell]._immovableWalls.size(); j++) {
renderWall(cell, _current._cells[cell]._immovableWalls[j], immovableAreOuter);
}
}
// TODO: which frame?
room->selectFrame(kMinotaurImage, getMinotaurZ(), 30,
getTileBase(_minotaurX, _minotaurY) - Common::Point(/*114, 117*/30+82, 33*2+52));
}
int getMinotaurZ() {
if (_minotaurX >= 5) {
return 6500;
}
if (_minotaurX < 0) {
return 4500;
}
if (_minotaurY >= 5) {
return 5960;
}
if (_minotaurY < 0) {
return 4500;
}
return 5000 + 150 * (_minotaurX + _minotaurY) + 60;
}
void renderWall(int cell, Wall &wall, bool outer) {
Common::Point delta;
if (wall._inTransit) {
wall._inTransit--;
g_vm->getVideoRoom()->selectFrame(
LayerId(kMaterialsMoveImage, cell, Common::String("to-") + dirnames[wall._position]),
getWallZ(cell, wall._position, outer),
(wall._strength - kStrengthStraw) * 4 + (wall._position + 1) % 4,
getTileBase(cell) + Common::Point(-40, -88));
g_vm->addTimer(kRerenderLabyrinth, kDefaultSpeed);
return;
}
switch (wall._position) {
case kUp:
delta = xVector + xVector + yVector + Common::Point(-8, -3) +
(outer ? Common::Point(0, 0) : Common::Point(+7, +5));
break;
case kDown:
delta = xVector + yVector + Common::Point(-8, -3) +
(outer ? Common::Point(+7, +5) : Common::Point(0, 0));
break;
case kLeft:
delta = xVector + Common::Point(0, -33)
+ (outer ? Common::Point(-7, +5) : Common::Point(0, 0));
break;
case kRight:
delta = xVector + yVector + Common::Point(0, -33)
+ (outer ? Common::Point(0, 0) : Common::Point(-7, +5));
break;
}
Common::Point pos = getTileBase(cell) + delta;
LayerId layer(kMaterialsImage, cell,
Common::String(dirnames[wall._position]) +
(outer ? "outer" : "inner"));
int frame = (wall._strength - kStrengthStraw) * 2 + (wall._position % 2);
g_vm->getVideoRoom()->selectFrame(layer, getWallZ(cell, wall._position, outer), frame, pos);
}
int getWallZ(int cell, Position pos, bool outer) {
int zValue = 150 * (cell / 5 + cell % 5) + 5000;
switch (pos) {
case kUp:
zValue += outer? 110 : 100;
break;
case kDown:
zValue += outer ? -10 : 0;
break;
case kLeft:
zValue += outer ? 40 : 50;
break;
case kRight:
zValue += outer ? 80 : 70;
break;
}
return zValue;
}
Common::Point getTileBase(int x, int y) {
return Common::Point(320, 456) + xVector * x + yVector * y;
}
Common::Point getTileBase(int id) {
return getTileBase(id / 5, id % 5);
}
void rotate(int cell) {
for (int j = 0;
j < (int) _current._cells[cell]._movableWalls.size();
j++) {
_current._cells[cell]._movableWalls[j]._position
= (Position) ((_current._cells[cell]._movableWalls[j]._position + 1) % 4);
_current._cells[cell]._movableWalls[j]._inTransit = 1;
}
}
void loadPuzzle(const Common::String &name) {
Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
Common::SharedPtr<Common::SeekableReadStream> gameStream(room->openFile(name + ".mcf"));
Common::SharedPtr<Common::SeekableReadStream> solStream(room->openFile(name + ".sol"));
Common::SharedPtr<Common::SeekableReadStream> cwStream(room->openFile(name + ".cw"));
readLabStream(_current, gameStream);
readLabStream(_solution, solStream);
for (int cell = 0; cell < numSquares; cell++) {
room->setHotzoneEnabled(Common::String::format("%d", cell),
!_current._cells[cell]._movableWalls.empty());
}
}
static void readLabStream(Labyrinth &lab, Common::SharedPtr<Common::SeekableReadStream> stream) {
stream->readLine(); // Level number
int gridSize = stream->readLine().asUint64();
if (gridSize == 0)
gridSize = 1;
stream->readLine(); // ?
stream->readLine(); // ?
int numLines = stream->readLine().asUint64();
// Extra walls
if (gridSize == 3 || gridSize == 4) {
Wall w;
w._isCritical = false;
w._id = -1;
w._strength = kStrengthStone;
w._position = kRight;
if (gridSize == 3)
lab._cells[4]._immovableWalls.push_back(w);
lab._cells[24]._immovableWalls.push_back(w);
}
for (int i = 0; i < numLines; i++) {
Common::String line = stream->readLine();
size_t cur = 0;
int rawcellid = line.asUint64();
int transformedcellid =
5 * (rawcellid / gridSize) +
(rawcellid % gridSize) + (5 - gridSize)
+ 5 * ((5 - gridSize) / 2);
cur = line.findFirstNotOf(kDigits, cur);
cur = line.findFirstOf(kDigits, cur);
int numWalls = line.substr(cur).asUint64();
cur = line.findFirstNotOf(kDigits, cur);
cur = line.findFirstOf(kDigits, cur);
/*int rotatable =*/ line.substr(cur).asUint64();
cur = line.findFirstNotOf(kDigits, cur);
cur = line.findFirstOf(kDigits, cur);
for (int j = 0; j < numWalls; j++) {
Wall w;
w._isCritical = false;
w._id = line.substr(cur).asUint64();
cur = line.findFirstNotOf(kDigits, cur);
cur = line.findFirstOf(kDigits, cur);
int pos = line.substr(cur).asUint64();
cur = line.findFirstNotOf(kDigits, cur);
cur = line.findFirstOf(kDigits, cur);
w._strength = (Strength) line.substr(cur).asUint64();
cur = line.findFirstNotOf(kDigits, cur);
cur = line.findFirstOf(kDigits, cur);
switch (pos % 4) {
case 0:
w._position = kLeft;
break;
case 1:
w._position = kUp;
break;
case 2:
w._position = kRight;
break;
case 3:
w._position = kDown;
break;
}
if (pos >= 4) {
lab._cells[transformedcellid]._immovableWalls.push_back(w);
} else {
lab._cells[transformedcellid]._movableWalls.push_back(w);
}
}
}
}
Common::Point xVector;
Common::Point yVector;
int _highlight;
DaedalusDialogState _dialogState;
int _minotaurX;
int _minotaurY;
int _minotaurTileId;
int _lastChargeSound;
int _lastTrappedSound;
int _lastEscapedSound;
int _levelId;
int _currentSound;
int _soundCounter;
int _soundMax;
Labyrinth _current;
Labyrinth _solution;
};
Common::SharedPtr<Hadesch::Handler> makeMinotaurHandler() {
return Common::SharedPtr<Hadesch::Handler>(new MinotaurHandler());
}
}