Initial commit
This commit is contained in:
395
engines/alcachofa/player.cpp
Normal file
395
engines/alcachofa/player.cpp
Normal file
@@ -0,0 +1,395 @@
|
||||
/* 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 "alcachofa/player.h"
|
||||
#include "alcachofa/script.h"
|
||||
#include "alcachofa/alcachofa.h"
|
||||
#include "alcachofa/menu.h"
|
||||
|
||||
using namespace Common;
|
||||
|
||||
namespace Alcachofa {
|
||||
|
||||
Player::Player()
|
||||
: _activeCharacter(&g_engine->world().mortadelo())
|
||||
, _semaphore("player") {
|
||||
const auto &cursorPath = g_engine->world().getGlobalAnimationName(GlobalAnimationKind::Cursor);
|
||||
_cursorAnimation.reset(new Animation(cursorPath));
|
||||
_cursorAnimation->load();
|
||||
}
|
||||
|
||||
void Player::preUpdate() {
|
||||
_selectedObject = nullptr;
|
||||
_cursorFrameI = 0;
|
||||
}
|
||||
|
||||
void Player::postUpdate() {
|
||||
if (g_engine->input().wasAnyMouseReleased())
|
||||
_pressedObject = nullptr;
|
||||
}
|
||||
|
||||
void Player::resetCursor() {
|
||||
_cursorFrameI = 0;
|
||||
}
|
||||
|
||||
void Player::updateCursor() {
|
||||
if (g_engine->menu().isOpen() || !_isGameLoaded)
|
||||
_cursorFrameI = 0;
|
||||
else if (_selectedObject == nullptr)
|
||||
_cursorFrameI = !g_engine->input().isMouseLeftDown() || _pressedObject != nullptr ? 6 : 7;
|
||||
else {
|
||||
auto type = _selectedObject->cursorType();
|
||||
switch (type) {
|
||||
case CursorType::LeaveUp:
|
||||
_cursorFrameI = 8;
|
||||
break;
|
||||
case CursorType::LeaveRight:
|
||||
_cursorFrameI = 10;
|
||||
break;
|
||||
case CursorType::LeaveDown:
|
||||
_cursorFrameI = 12;
|
||||
break;
|
||||
case CursorType::LeaveLeft:
|
||||
_cursorFrameI = 14;
|
||||
break;
|
||||
case CursorType::WalkTo:
|
||||
_cursorFrameI = 6;
|
||||
break;
|
||||
case CursorType::Point:
|
||||
default:
|
||||
_cursorFrameI = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (_cursorFrameI != 0) {
|
||||
if (g_engine->input().isAnyMouseDown() && _pressedObject == _selectedObject)
|
||||
_cursorFrameI++;
|
||||
} else if (g_engine->input().isMouseLeftDown())
|
||||
_cursorFrameI = 2;
|
||||
else if (g_engine->input().isMouseRightDown())
|
||||
_cursorFrameI = 4;
|
||||
}
|
||||
}
|
||||
|
||||
void Player::drawCursor(bool forceDefaultCursor) {
|
||||
Point cursorPos = g_engine->input().mousePos2D();
|
||||
if (_heldItem == nullptr || forceDefaultCursor) {
|
||||
if (forceDefaultCursor)
|
||||
_cursorFrameI = 0;
|
||||
g_engine->drawQueue().add<AnimationDrawRequest>(_cursorAnimation.get(), _cursorFrameI, as2D(cursorPos), -10);
|
||||
} else {
|
||||
auto itemGraphic = _heldItem->graphic();
|
||||
assert(itemGraphic != nullptr);
|
||||
auto &animation = itemGraphic->animation();
|
||||
auto frameOffset = animation.totalFrameOffset(0);
|
||||
auto imageSize = animation.imageSize(animation.imageIndex(0, 0));
|
||||
cursorPos -= frameOffset + imageSize / 2;
|
||||
g_engine->drawQueue().add<AnimationDrawRequest>(&animation, 0, as2D(cursorPos), -kForegroundOrderCount);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera, bool isTemporary) {
|
||||
debugC(1, kDebugGameplay, "Change room to %s", targetRoomName.c_str());
|
||||
|
||||
// original would be to always free all resources from globalRoom, inventory, GlobalUI
|
||||
// We don't do that, it is unnecessary, all resources would be loaded right after
|
||||
// Instead we just keep resources loaded for all global rooms and during inventory/room transitions
|
||||
|
||||
if (targetRoomName.equalsIgnoreCase("SALIR")) {
|
||||
_currentRoom = nullptr;
|
||||
return; // exiting game entirely
|
||||
}
|
||||
|
||||
if (_currentRoom != nullptr) {
|
||||
g_engine->scheduler().killProcessByName("ACTUALIZAR_" + _currentRoom->name());
|
||||
|
||||
bool keepResources =
|
||||
_currentRoom->name().equalsIgnoreCase(targetRoomName) ||
|
||||
_currentRoom->name().equalsIgnoreCase("inventario");
|
||||
if (targetRoomName.equalsIgnoreCase("inventario")) {
|
||||
keepResources = true;
|
||||
if (!_isInTemporaryRoom)
|
||||
_roomBeforeInventory = _currentRoom;
|
||||
}
|
||||
if (!keepResources)
|
||||
_currentRoom->freeResources();
|
||||
}
|
||||
|
||||
// this fixes a bug with all original games where changing the room in the inventory (e.g. iFOTO in aventura de cine)
|
||||
// would overwrite the actual game room thus returning from the inventory one would be stuck in the temporary room
|
||||
// If we know that a transition is temporary we prevent that and only remember the real game room
|
||||
_isInTemporaryRoom = isTemporary;
|
||||
|
||||
_currentRoom = g_engine->world().getRoomByName(targetRoomName.c_str());
|
||||
if (_currentRoom == nullptr) // no good way to recover, leaving-the-room actions might already prevent further progress
|
||||
error("Invalid room name: %s", targetRoomName.c_str());
|
||||
|
||||
if (!_didLoadGlobalRooms) {
|
||||
_didLoadGlobalRooms = true;
|
||||
g_engine->world().inventory().loadResources();
|
||||
g_engine->world().globalRoom().loadResources();
|
||||
}
|
||||
_currentRoom->loadResources(); // if we kept resources we loop over a couple noops, that is fine.
|
||||
|
||||
if (resetCamera)
|
||||
g_engine->camera().resetRotationAndScale();
|
||||
WalkingCharacter *followTarget = g_engine->camera().followTarget();
|
||||
if (followTarget != nullptr)
|
||||
g_engine->camera().setFollow(followTarget, true);
|
||||
_pressedObject = _selectedObject = nullptr;
|
||||
}
|
||||
|
||||
void Player::changeRoomToBeforeInventory() {
|
||||
assert(_roomBeforeInventory != nullptr);
|
||||
changeRoom(_roomBeforeInventory->name(), true);
|
||||
_roomBeforeInventory = nullptr;
|
||||
}
|
||||
|
||||
MainCharacter *Player::inactiveCharacter() const {
|
||||
if (_activeCharacter == nullptr)
|
||||
return nullptr;
|
||||
return &g_engine->world().getOtherMainCharacterByKind(activeCharacterKind());
|
||||
}
|
||||
|
||||
FakeSemaphore &Player::semaphoreFor(MainCharacterKind kind) {
|
||||
static FakeSemaphore dummySemaphore("dummy");
|
||||
switch (kind) {
|
||||
case MainCharacterKind::None:
|
||||
return _semaphore;
|
||||
case MainCharacterKind::Mortadelo:
|
||||
return g_engine->world().mortadelo().semaphore();
|
||||
case MainCharacterKind::Filemon:
|
||||
return g_engine->world().filemon().semaphore();
|
||||
default:
|
||||
assert(false && "Invalid main character kind");
|
||||
return dummySemaphore;
|
||||
}
|
||||
}
|
||||
|
||||
void Player::triggerObject(ObjectBase *object, const char *action) {
|
||||
assert(object != nullptr && action != nullptr);
|
||||
if (_activeCharacter->isBusy() || _activeCharacter->currentlyUsing() != nullptr)
|
||||
return;
|
||||
debugC(1, kDebugGameplay, "Trigger object %s %s with %s", object->typeName(), object->name().c_str(), action);
|
||||
|
||||
if (strcmp(action, "MIRAR") == 0 || inactiveCharacter()->currentlyUsing() == object) {
|
||||
action = "MIRAR";
|
||||
_activeCharacter->currentlyUsing() = nullptr;
|
||||
} else
|
||||
_activeCharacter->currentlyUsing() = object;
|
||||
|
||||
auto &script = g_engine->script();
|
||||
if (script.createProcess(activeCharacterKind(), object->name(), action, ScriptFlags::AllowMissing) != nullptr)
|
||||
return;
|
||||
|
||||
_activeCharacter->currentlyUsing() = nullptr;
|
||||
if (scumm_stricmp(action, "MIRAR") == 0)
|
||||
script.createProcess(activeCharacterKind(), "DefectoMirar");
|
||||
//else if (action[0] == 'i' && object->name()[0] == 'i')
|
||||
// This case can happen if you combine two objects without procedure, the original engine
|
||||
// would attempt to start the procedure "DefectoObjeto" which does not exist
|
||||
// (this should be revised when working on further games)
|
||||
else
|
||||
script.createProcess(activeCharacterKind(), "DefectoUsar");
|
||||
}
|
||||
|
||||
struct DoorTask final : public Task {
|
||||
DoorTask(Process &process, const Door *door, FakeLock &&lock)
|
||||
: Task(process)
|
||||
, _lock(move(lock))
|
||||
, _sourceDoor(door)
|
||||
, _character(g_engine->player().activeCharacter())
|
||||
, _player(g_engine->player())
|
||||
, _targetObject(nullptr)
|
||||
, _targetDirection(Direction::Invalid) {
|
||||
findTarget();
|
||||
process.name() = String::format("Door to %s %s",
|
||||
_targetRoom == nullptr ? "<null>" : _targetRoom->name().c_str(),
|
||||
_targetObject == nullptr ? "<null>" : _targetObject->name().c_str());
|
||||
}
|
||||
|
||||
DoorTask(Process &process, Serializer &s)
|
||||
: Task(process)
|
||||
, _player(g_engine->player()) {
|
||||
DoorTask::syncGame(s);
|
||||
}
|
||||
|
||||
TaskReturn run() override {
|
||||
TASK_BEGIN;
|
||||
if (_targetRoom == nullptr || _targetObject == nullptr)
|
||||
return TaskReturn::finish(1);
|
||||
|
||||
_musicLock = FakeLock("door-music", g_engine->sounds().musicSemaphore());
|
||||
if (g_engine->sounds().musicID() != _targetRoom->musicID())
|
||||
g_engine->sounds().fadeMusic();
|
||||
TASK_WAIT(1, fade(process(), FadeType::ToBlack, 0, 1, 500, EasingType::Out, -5));
|
||||
_player.changeRoom(_targetRoom->name(), true); //-V779
|
||||
|
||||
if (_targetRoom->fixedCameraOnEntering())
|
||||
g_engine->camera().setPosition(as2D(_targetObject->interactionPoint()));
|
||||
else {
|
||||
_character->room() = _targetRoom;
|
||||
_character->setPosition(_targetObject->interactionPoint());
|
||||
_character->stopWalking(_targetDirection);
|
||||
g_engine->camera().setFollow(_character, true);
|
||||
}
|
||||
|
||||
g_engine->sounds().setMusicToRoom(_targetRoom->musicID());
|
||||
_musicLock.release();
|
||||
|
||||
if (g_engine->script().createProcess(_character->kind(), "ENTRAR_" + _targetRoom->name(), ScriptFlags::AllowMissing))
|
||||
TASK_YIELD(2);
|
||||
else
|
||||
TASK_WAIT(3, fade(process(), FadeType::ToBlack, 1, 0, 500, EasingType::Out, -5));
|
||||
TASK_END;
|
||||
}
|
||||
|
||||
void debugPrint() override {
|
||||
g_engine->console().debugPrintf("%s\n", process().name().c_str());
|
||||
}
|
||||
|
||||
void syncGame(Serializer &s) override {
|
||||
assert(s.isSaving() || (_lock.isReleased() && _musicLock.isReleased()));
|
||||
|
||||
Task::syncGame(s);
|
||||
syncObjectAsString(s, _sourceDoor);
|
||||
syncObjectAsString(s, _character);
|
||||
bool hasMusicLock = !_musicLock.isReleased();
|
||||
s.syncAsByte(hasMusicLock);
|
||||
if (s.isLoading() && hasMusicLock)
|
||||
_musicLock = FakeLock("door-music", g_engine->sounds().musicSemaphore());
|
||||
|
||||
_lock = FakeLock("door", _character->semaphore());
|
||||
findTarget();
|
||||
}
|
||||
|
||||
const char *taskName() const override;
|
||||
|
||||
private:
|
||||
void findTarget() {
|
||||
_targetRoom = g_engine->world().getRoomByName(_sourceDoor->targetRoom().c_str());
|
||||
if (_targetRoom == nullptr) {
|
||||
g_engine->game().unknownDoorTargetRoom(_sourceDoor->targetRoom());
|
||||
return;
|
||||
}
|
||||
|
||||
_targetObject = dynamic_cast<InteractableObject *>(_targetRoom->getObjectByName(_sourceDoor->targetObject().c_str()));
|
||||
if (_targetObject == nullptr) {
|
||||
g_engine->game().unknownDoorTargetDoor(_sourceDoor->targetRoom(), _sourceDoor->targetObject());
|
||||
return;
|
||||
}
|
||||
|
||||
_targetDirection = _sourceDoor->characterDirection();
|
||||
}
|
||||
|
||||
FakeLock _lock, _musicLock;
|
||||
const Door *_sourceDoor = nullptr;
|
||||
const InteractableObject *_targetObject = nullptr;
|
||||
Direction _targetDirection = {};
|
||||
Room *_targetRoom = nullptr;
|
||||
MainCharacter *_character = nullptr;
|
||||
Player &_player;
|
||||
};
|
||||
DECLARE_TASK(DoorTask)
|
||||
|
||||
void Player::triggerDoor(const Door *door) {
|
||||
_heldItem = nullptr;
|
||||
|
||||
if (g_engine->game().shouldTriggerDoor(door)) {
|
||||
FakeLock lock("door", _activeCharacter->semaphore());
|
||||
g_engine->scheduler().createProcess<DoorTask>(activeCharacterKind(), door, move(lock));
|
||||
}
|
||||
}
|
||||
|
||||
// the last dialog character mechanic seems like a hack in the original engine
|
||||
// all talking characters (see SayText kernel call) are added to a fixed-size
|
||||
// rolling queue and stopped upon killProcesses
|
||||
|
||||
void Player::addLastDialogCharacter(Character *character) {
|
||||
auto lastDialogCharactersEnd = _lastDialogCharacters + kMaxLastDialogCharacters;
|
||||
if (Common::find(_lastDialogCharacters, lastDialogCharactersEnd, character) != lastDialogCharactersEnd)
|
||||
return;
|
||||
_lastDialogCharacters[_nextLastDialogCharacter++] = character;
|
||||
_nextLastDialogCharacter %= kMaxLastDialogCharacters;
|
||||
}
|
||||
|
||||
void Player::stopLastDialogCharacters() {
|
||||
// originally only the isTalking flag is reset, but this seems a bit safer so unless we find a bug
|
||||
for (int i = 0; i < kMaxLastDialogCharacters; i++) {
|
||||
auto character = _lastDialogCharacters[i];
|
||||
if (character != nullptr)
|
||||
character->resetTalking();
|
||||
}
|
||||
}
|
||||
|
||||
void Player::setActiveCharacter(MainCharacterKind kind) {
|
||||
scumm_assert(kind == MainCharacterKind::Mortadelo || kind == MainCharacterKind::Filemon);
|
||||
_activeCharacter = &g_engine->world().getMainCharacterByKind(kind);
|
||||
}
|
||||
|
||||
bool Player::isAllowedToOpenMenu() {
|
||||
return
|
||||
isGameLoaded() &&
|
||||
!g_engine->menu().isOpen() &&
|
||||
g_engine->sounds().musicSemaphore().isReleased() &&
|
||||
!g_engine->script().variable("prohibirESC") &&
|
||||
!_isInTemporaryRoom; // we cannot reliably store this state across multiple room changes
|
||||
}
|
||||
|
||||
void Player::syncGame(Serializer &s) {
|
||||
auto characterKind = activeCharacterKind();
|
||||
syncEnum(s, characterKind);
|
||||
switch (characterKind) {
|
||||
case MainCharacterKind::None:
|
||||
_activeCharacter = nullptr;
|
||||
break;
|
||||
case MainCharacterKind::Mortadelo:
|
||||
case MainCharacterKind::Filemon:
|
||||
_activeCharacter = &g_engine->world().getMainCharacterByKind(characterKind);
|
||||
break;
|
||||
default:
|
||||
error("Invalid character kind in savestate: %d", (int)characterKind);
|
||||
}
|
||||
|
||||
FakeSemaphore::sync(s, _semaphore);
|
||||
|
||||
String roomName;
|
||||
if (s.isSaving()) {
|
||||
roomName =
|
||||
g_engine->menu().isOpen() ? g_engine->menu().previousRoom()->name() // save from in-game menu
|
||||
: _roomBeforeInventory != nullptr ? _roomBeforeInventory->name() // save from ScummVM while in inventory
|
||||
: currentRoom()->name(); // save from ScumnmVM global menu or autosave in normal gameplay
|
||||
}
|
||||
s.syncString(roomName);
|
||||
if (s.isLoading()) {
|
||||
_selectedObject = nullptr;
|
||||
_pressedObject = nullptr;
|
||||
_heldItem = nullptr;
|
||||
_nextLastDialogCharacter = 0;
|
||||
_isGameLoaded = true;
|
||||
_roomBeforeInventory = nullptr;
|
||||
_isInTemporaryRoom = false;
|
||||
fill(_lastDialogCharacters, _lastDialogCharacters + kMaxLastDialogCharacters, nullptr);
|
||||
changeRoom(roomName, true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user