/* 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 .
*
*/
#include "alcachofa/objects.h"
#include "alcachofa/rooms.h"
#include "alcachofa/script.h"
#include "alcachofa/global-ui.h"
#include "alcachofa/alcachofa.h"
using namespace Common;
using namespace Math;
namespace Alcachofa {
const char *Item::typeName() const { return "Item"; }
Item::Item(Room *room, ReadStream &stream)
: GraphicObject(room, stream) {
stream.readByte(); // unused and ignored byte
}
Item::Item(const Item &other)
: GraphicObject(other.room(), other.name().c_str()) {
_type = other._type;
_posterizeAlpha = other._posterizeAlpha;
_graphic.~Graphic();
new (&_graphic) Graphic(other._graphic);
}
void Item::draw() {
if (!isEnabled())
return;
Item *heldItem = g_engine->player().heldItem();
if (heldItem == nullptr || !heldItem->name().equalsIgnoreCase(name()))
GraphicObject::draw();
}
void Item::trigger() {
auto &player = g_engine->player();
auto &heldItem = player.heldItem();
if (g_engine->input().wasMouseRightReleased()) {
if (heldItem == nullptr)
player.triggerObject(this, "MIRAR");
else
heldItem = nullptr;
} else if (heldItem == nullptr)
heldItem = this;
else if (g_engine->script().hasProcedure(name(), heldItem->name()) ||
!g_engine->script().hasProcedure(heldItem->name(), name()))
player.triggerObject(this, heldItem->name().c_str());
else
player.triggerObject(heldItem, name().c_str());
}
ITriggerableObject::ITriggerableObject(ReadStream &stream)
: _interactionPoint(Shape(stream).firstPoint())
, _interactionDirection((Direction)stream.readSint32LE()) {}
void ITriggerableObject::onClick() {
auto heldItem = g_engine->player().heldItem();
const char *action;
if (heldItem == nullptr)
action = g_engine->input().wasMouseLeftReleased() ? "MIRAR" : "PULSAR";
else
action = heldItem->name().c_str();
g_engine->player().activeCharacter()->walkTo(_interactionPoint, Direction::Invalid, this, action);
}
const char *InteractableObject::typeName() const { return "InteractableObject"; }
InteractableObject::InteractableObject(Room *room, ReadStream &stream)
: PhysicalObject(room, stream)
, ITriggerableObject(stream)
, _relatedObject(readVarString(stream)) {
_relatedObject.toUppercase();
}
void InteractableObject::drawDebug() {
auto renderer = dynamic_cast(&g_engine->renderer());
if (!g_engine->console().showInteractables() || renderer == nullptr || !isEnabled())
return;
renderer->debugShape(*shape());
}
void InteractableObject::onClick() {
ITriggerableObject::onClick();
onHoverUpdate();
}
void InteractableObject::trigger(const char *action) {
g_engine->player().activeCharacter()->stopWalking();
g_engine->player().triggerObject(this, action);
}
void InteractableObject::toggle(bool isEnabled) {
ObjectBase::toggle(isEnabled);
ObjectBase *related = room()->getObjectByName(_relatedObject.c_str());
if (related != nullptr)
related->toggle(isEnabled);
}
const char *Door::typeName() const { return "Door"; }
Door::Door(Room *room, ReadStream &stream)
: InteractableObject(room, stream)
, _targetRoom(readVarString(stream))
, _targetObject(readVarString(stream))
, _characterDirection((Direction)stream.readSint32LE()) {
_targetRoom.replace(' ', '_');
}
CursorType Door::cursorType() const {
CursorType fromObject = ShapeObject::cursorType();
if (fromObject != CursorType::Point)
return fromObject;
switch (_interactionDirection) {
case Direction::Up:
return CursorType::LeaveUp;
case Direction::Right:
return CursorType::LeaveRight;
case Direction::Down:
return CursorType::LeaveDown;
case Direction::Left:
return CursorType::LeaveLeft;
default:
assert(false && "Invalid door character direction");
return fromObject;
}
}
void Door::onClick() {
if (g_engine->getMillis() - _lastClickTime < 500 && g_engine->player().activeCharacter()->clearTargetIf(this))
trigger(nullptr);
else {
InteractableObject::onClick();
_lastClickTime = g_engine->getMillis();
}
}
void Door::trigger(const char *_) {
g_engine->player().triggerDoor(this);
}
const char *Character::typeName() const { return "Character"; }
Character::Character(Room *room, ReadStream &stream)
: ShapeObject(room, stream)
, ITriggerableObject(stream)
, _graphicNormal(stream)
, _graphicTalking(stream) {
_graphicNormal.start(true);
_graphicNormal.frameI() = _graphicTalking.frameI() = 0;
_order = _graphicNormal.order();
}
static Graphic *graphicOf(ObjectBase *object, Graphic *fallback = nullptr) {
auto objectGraphic = object == nullptr ? nullptr : object->graphic();
return objectGraphic == nullptr ? fallback : objectGraphic;
}
void Character::update() {
if (!isEnabled())
return;
updateSelection();
Graphic *animateGraphic = graphicOf(_curAnimateObject);
if (animateGraphic != nullptr) {
animateGraphic->topLeft() = Point(0, 0);
animateGraphic->update();
} else if (_isTalking)
updateTalkingAnimation();
else if (g_engine->world().somebodyUsing(this)) {
Graphic *talkGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
talkGraphic->start(true);
talkGraphic->pause();
talkGraphic->update();
} else
_graphicNormal.update();
}
void Character::updateTalkingAnimation() {
Graphic *talkGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
if (!_isTalking) {
talkGraphic->reset();
return;
}
if (talkGraphic == &_graphicTalking && !_isSpeaking)
talkGraphic->reset();
talkGraphic->update();
}
void Character::draw() {
if (!isEnabled())
return;
Graphic *activeGraphic = graphic();
assert(activeGraphic != nullptr);
if (activeGraphic->hasAnimation())
g_engine->drawQueue().add(*activeGraphic, true, BlendMode::AdditiveAlpha, _lodBias);
}
void Character::drawDebug() {
auto renderer = dynamic_cast(&g_engine->renderer());
if (!g_engine->console().showCharacters() || renderer == nullptr || !isEnabled())
return;
renderer->debugShape(*shape());
}
void Character::loadResources() {
_graphicNormal.loadResources();
_graphicTalking.loadResources();
}
void Character::freeResources() {
_graphicNormal.freeResources();
_graphicTalking.freeResources();
}
void Character::syncGame(Serializer &serializer) {
ShapeObject::syncGame(serializer);
serializer.syncAsByte(_isTalking);
serializer.syncAsSint32LE(_curDialogId);
_graphicNormal.syncGame(serializer);
_graphicTalking.syncGame(serializer);
syncObjectAsString(serializer, _curAnimateObject);
syncObjectAsString(serializer, _curTalkingObject);
serializer.syncAsFloatLE(_lodBias);
}
void Character::syncObjectAsString(Serializer &serializer, ObjectBase *&object) {
String name;
if (serializer.isSaving() && object != nullptr)
name = object->name();
serializer.syncString(name);
if (serializer.isLoading()) {
if (name.empty())
object = nullptr;
else {
object = room()->getObjectByName(name.c_str());
if (object == nullptr)
object = room()->world().getObjectByName(name.c_str());
if (object == nullptr)
g_engine->game().unknownSerializedObject(
name.c_str(), this->name().c_str(), room()->name().c_str());
}
}
}
Graphic *Character::graphic() {
Graphic *activeGraphic = graphicOf(_curAnimateObject);
if (activeGraphic == nullptr && (_isTalking || g_engine->world().somebodyUsing(this)))
activeGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
if (activeGraphic == nullptr)
activeGraphic = &_graphicNormal;
return activeGraphic;
}
void Character::onClick() {
ITriggerableObject::onClick();
onHoverUpdate();
}
void Character::trigger(const char *action) {
g_engine->player().activeCharacter()->stopWalking(_interactionDirection);
if (g_engine->game().shouldCharacterTrigger(this, action))
g_engine->player().triggerObject(this, action);
}
struct SayTextTask final : public Task {
SayTextTask(Process &process, Character *character, int32 dialogId)
: Task(process)
, _character(character)
, _dialogId(dialogId) {}
SayTextTask(Process &process, Serializer &s)
: Task(process) {
syncGame(s);
}
TaskReturn run() override {
bool isSoundStillPlaying;
TASK_BEGIN;
_character->_isTalking = true;
graphicOf(_character->_curTalkingObject, &_character->_graphicTalking)->start(true);
while (true) {
g_engine->player().addLastDialogCharacter(_character);
if (_soundHandle == SoundHandle {}) {
bool hasMortadeloVoice = g_engine->game().hasMortadeloVoice(_character);
_soundHandle = g_engine->sounds().playVoice(
String::format(hasMortadeloVoice ? "M%04d" : "%04d", _dialogId),
0);
}
isSoundStillPlaying = g_engine->sounds().isAlive(_soundHandle);
g_engine->sounds().setAppropriateVolume(_soundHandle, process().character(), _character);
if (!isSoundStillPlaying || g_engine->input().wasAnyMouseReleased())
_character->_isTalking = false;
if (g_engine->config().subtitles() &&
process().isActiveForPlayer()) {
g_engine->drawQueue().add(
g_engine->globalUI().dialogFont(),
g_engine->world().getDialogLine(_dialogId),
Point(g_system->getWidth() / 2, g_system->getHeight() - 200),
-1, true, kWhite, -kForegroundOrderCount);
}
if (!_character->_isTalking) {
g_engine->sounds().fadeOut(_soundHandle, 100);
TASK_WAIT(1, delay(200));
TASK_RETURN(0); //-V779
}
_character->isSpeaking() = !isSoundStillPlaying ||
g_engine->sounds().isNoisy(_soundHandle, 80.0f, 150.0f);
TASK_YIELD(2);
}
TASK_END;
}
void debugPrint() override {
g_engine->console().debugPrintf("SayText %s, %d\n", _character->name().c_str(), _dialogId);
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
syncObjectAsString(s, _character);
s.syncAsSint32LE(_dialogId);
}
const char *taskName() const override;
private:
Character *_character = nullptr;
int32 _dialogId = 0;
SoundHandle _soundHandle = {};
};
DECLARE_TASK(SayTextTask)
Task *Character::sayText(Process &process, int32 dialogId) {
return new SayTextTask(process, this, dialogId);
}
void Character::resetTalking() {
_isTalking = false;
_curDialogId = -1;
_curTalkingObject = nullptr;
}
void Character::talkUsing(ObjectBase *talkObject) {
_curTalkingObject = talkObject;
if (talkObject == nullptr)
return;
auto graphic = talkObject->graphic();
if (graphic == nullptr)
error("Talk object %s does not have a graphic", talkObject->name().c_str());
graphic->start(true);
if (room() == g_engine->player().currentRoom())
graphic->update();
}
struct AnimateCharacterTask final : public Task {
AnimateCharacterTask(Process &process, Character *character, ObjectBase *animateObject)
: Task(process)
, _character(character)
, _animateObject(animateObject)
, _graphic(animateObject->graphic()) {
scumm_assert(_graphic != nullptr);
}
AnimateCharacterTask(Process &process, Serializer &s)
: Task(process) {
syncGame(s);
}
TaskReturn run() override {
TASK_BEGIN;
while (_character->_curAnimateObject != nullptr)
TASK_YIELD(1);
_character->_curAnimateObject = _animateObject;
_graphic->start(false);
if (_character->room() == g_engine->player().currentRoom())
_graphic->update();
do {
TASK_YIELD(2);
if (process().isActiveForPlayer() && g_engine->input().wasAnyMouseReleased()) //-V779
_graphic->pause();
} while (!_graphic->isPaused());
_character->_curAnimateObject = nullptr;
_character->_curTalkingObject = nullptr;
TASK_END;
}
void debugPrint() override {
g_engine->console().debugPrintf("AnimateCharacter %s, %s\n", _character->name().c_str(), _animateObject->name().c_str());
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
syncObjectAsString(s, _character);
syncObjectAsString(s, _animateObject);
_graphic = _animateObject->graphic();
scumm_assert(_graphic != nullptr);
}
const char *taskName() const override;
private:
Character *_character = nullptr;
ObjectBase *_animateObject = nullptr;
Graphic *_graphic = nullptr;
};
DECLARE_TASK(AnimateCharacterTask)
Task *Character::animate(Process &process, ObjectBase *animateObject) {
assert(animateObject != nullptr);
return new AnimateCharacterTask(process, this, animateObject);
}
struct LerpLodBiasTask final : public Task {
LerpLodBiasTask(Process &process, Character *character, float targetLodBias, uint32 durationMs)
: Task(process)
, _character(character)
, _targetLodBias(targetLodBias)
, _durationMs(durationMs) {}
LerpLodBiasTask(Process &process, Serializer &s)
: Task(process) {
syncGame(s);
}
TaskReturn run() override {
TASK_BEGIN;
_startTime = g_engine->getMillis();
_sourceLodBias = _character->lodBias();
while (g_engine->getMillis() - _startTime < _durationMs) {
_character->lodBias() = _sourceLodBias + (_targetLodBias - _sourceLodBias) *
((g_engine->getMillis() - _startTime) / (float)_durationMs);
TASK_YIELD(1);
}
_character->lodBias() = _targetLodBias;
TASK_END;
}
void debugPrint() override {
uint32 remaining = g_engine->getMillis() - _startTime <= _durationMs
? _durationMs - (g_engine->getMillis() - _startTime)
: 0;
g_engine->console().debugPrintf("Lerp lod bias of %s to %f with %ums remaining\n",
_character->name().c_str(), _targetLodBias, remaining);
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
syncObjectAsString(s, _character);
s.syncAsFloatLE(_sourceLodBias);
s.syncAsFloatLE(_targetLodBias);
s.syncAsUint32LE(_startTime);
s.syncAsUint32LE(_durationMs);
}
const char *taskName() const override;
private:
Character *_character = nullptr;
float _sourceLodBias = 0, _targetLodBias = 0;
uint32 _startTime = 0, _durationMs = 0;
};
DECLARE_TASK(LerpLodBiasTask)
Task *Character::lerpLodBias(Process &process, float targetLodBias, int32 durationMs) {
return new LerpLodBiasTask(process, this, targetLodBias, durationMs);
}
const char *WalkingCharacter::typeName() const { return "WalkingCharacter"; }
WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
: Character(room, stream) {
for (int32 i = 0; i < kDirectionCount; i++) {
auto fileName = readVarString(stream);
_walkingAnimations[i].reset(new Animation(Common::move(fileName)));
}
for (int32 i = 0; i < kDirectionCount; i++) {
auto fileName = readVarString(stream);
_talkingAnimations[i].reset(new Animation(Common::move(fileName)));
}
}
void WalkingCharacter::update() {
Character::update();
if (!isEnabled())
return;
updateWalking();
auto activeFloor = room()->activeFloor();
if (activeFloor != nullptr) {
if (activeFloor->polygonContaining(_sourcePos) < 0)
_sourcePos = _currentPos = activeFloor->closestPointTo(_sourcePos);
if (activeFloor->polygonContaining(_currentPos) < 0)
_currentPos = activeFloor->closestPointTo(_currentPos);
}
if (!_isWalking) {
_graphicTalking.setAnimation(talkingAnimation());
updateTalkingAnimation();
_currentPos = _sourcePos;
}
_graphicNormal.topLeft() = _graphicTalking.topLeft() = _currentPos;
auto animateGraphic = graphicOf(_curAnimateObject);
auto talkingGraphic = graphicOf(_curTalkingObject);
if (animateGraphic != nullptr)
animateGraphic->topLeft() = _currentPos;
if (talkingGraphic != nullptr)
talkingGraphic->topLeft() = _currentPos;
if (room() != &g_engine->world().globalRoom()) {
float depth = room()->depthAt(_currentPos);
int8 order = room()->orderAt(_currentPos);
_graphicNormal.order() = _graphicTalking.order() = order;
_graphicNormal.depthScale() = _graphicTalking.depthScale() = depth;
if (animateGraphic != nullptr) {
animateGraphic->order() = order;
animateGraphic->depthScale() = depth;
}
if (talkingGraphic != nullptr) {
talkingGraphic->order() = order;
talkingGraphic->depthScale() = depth;
}
}
_interactionPoint = _currentPos;
_interactionDirection = Direction::Right;
if (this != g_engine->player().activeCharacter()) {
int16 interactionOffset = (int16)(150 * _graphicNormal.depthScale());
_interactionPoint.x -= interactionOffset;
if (activeFloor != nullptr && activeFloor->polygonContaining(_interactionPoint) < 0) {
_interactionPoint.x = _currentPos.x + interactionOffset;
_interactionDirection = Direction::Left;
}
}
}
static Direction getDirection(Point from, Point to) {
Point delta = from - to;
if (from.x == to.x)
return from.y < to.y ? Direction::Down : Direction::Up;
else if (from.x < to.x) {
int slope = 1000 * delta.y / -delta.x;
return slope > 1000 ? Direction::Up
: slope < -1000 ? Direction::Down
: Direction::Right;
} else { // from.x > to.x
int slope = 1000 * delta.y / delta.x;
return slope > 1000 ? Direction::Up
: slope < -1000 ? Direction::Down
: Direction::Left;
}
}
void WalkingCharacter::updateWalking() {
if (!_isWalking)
return;
static constexpr float kHigherStepSizeThreshold = 0x4CCC / 65535.0f;
static constexpr float kMinStepSizeFactor = 0x3333 / 65535.0f;
_stepSizeFactor = _graphicNormal.depthScale();
if (_stepSizeFactor < kHigherStepSizeThreshold)
_stepSizeFactor = _stepSizeFactor / 3.0f + kMinStepSizeFactor;
Point targetPos = _pathPoints.top();
if (_sourcePos == targetPos) {
_currentPos = targetPos;
_pathPoints.pop();
} else {
updateWalkingAnimation();
const int32 distanceToTarget = (int32)(sqrtf(_sourcePos.sqrDist(targetPos)));
if (_walkedDistance < distanceToTarget) {
// separated because having only 16 bits and multiplications seems dangerous
_currentPos.x = _sourcePos.x + _walkedDistance * (targetPos.x - _sourcePos.x) / distanceToTarget;
_currentPos.y = _sourcePos.y + _walkedDistance * (targetPos.y - _sourcePos.y) / distanceToTarget;
} else {
_sourcePos = _currentPos = targetPos;
_pathPoints.pop();
_walkedDistance = 1;
_lastWalkAnimFrame = 0;
}
}
if (_pathPoints.empty()) {
_isWalking = false;
_currentPos = _sourcePos = targetPos;
if (_endWalkingDirection != Direction::Invalid)
_direction = _endWalkingDirection;
onArrived();
}
_graphicNormal.topLeft() = _currentPos;
}
void WalkingCharacter::updateWalkingAnimation() {
_direction = getDirection(_sourcePos, _pathPoints.top());
auto animation = walkingAnimation();
_graphicNormal.setAnimation(animation);
// this is very confusing. Let's see what it does
const int32 halfFrameCount = (int32)animation->frameCount() / 2;
int32 expectedFrame = (int32)(g_engine->getMillis() - _graphicNormal.lastTime()) * 12 / 1000;
const bool isUnexpectedFrame = expectedFrame != _lastWalkAnimFrame;
int32 stepFrameFrom, stepFrameTo;
if (expectedFrame < halfFrameCount - 1) {
_lastWalkAnimFrame = expectedFrame;
stepFrameFrom = 2 * expectedFrame - 2;
stepFrameTo = 2 * expectedFrame;
} else {
const int32 frameThreshold = _lastWalkAnimFrame <= halfFrameCount - 1
? _lastWalkAnimFrame
: (_lastWalkAnimFrame - halfFrameCount + 1) % (halfFrameCount - 2) + 1;
_lastWalkAnimFrame = expectedFrame;
expectedFrame = (expectedFrame - halfFrameCount + 1) % (halfFrameCount - 2) + 1;
if (expectedFrame >= frameThreshold) {
stepFrameFrom = 2 * expectedFrame - 2;
stepFrameTo = 2 * expectedFrame;
} else {
stepFrameFrom = 2 * (halfFrameCount - 2);
stepFrameTo = 2 * halfFrameCount - 2;
}
}
if (isUnexpectedFrame) {
const float stepSize = sqrtf(animation->frameCenter(stepFrameFrom).sqrDist(animation->frameCenter(stepFrameTo)));
_walkedDistance += (int32)(stepSize * _stepSizeFactor);
}
_graphicNormal.frameI() = 2 * expectedFrame; // especially this: wtf?
}
void WalkingCharacter::onArrived() {}
void WalkingCharacter::stopWalking(Direction direction) {
// be careful, the original engine had two versions of this method
// one without resetting _sourcePos
_isWalking = false;
_sourcePos = _currentPos;
if (direction != Direction::Invalid)
_direction = direction;
}
void WalkingCharacter::walkTo(
Point target, Direction endDirection,
ITriggerableObject *activateObject, const char *activateAction) {
// all the activation parameters are only relevant for MainCharacter
if (_isWalking)
_sourcePos = _currentPos;
else {
_lastWalkAnimFrame = 0;
int32 prevWalkFrame = _graphicNormal.frameI();
_graphicNormal.reset();
_graphicNormal.frameI() = prevWalkFrame;
}
_pathPoints.clear();
auto floor = room()->activeFloor();
if (floor != nullptr)
floor->findPath(_sourcePos, target, _pathPoints);
if (_pathPoints.empty()) {
_isWalking = false;
onArrived();
return;
}
_isWalking = true;
_endWalkingDirection = endDirection;
_walkedDistance = 0;
updateWalking();
}
void WalkingCharacter::setPosition(Point target) {
_isWalking = false;
_sourcePos = _currentPos = target;
}
void WalkingCharacter::draw() {
if (!isEnabled())
return;
Graphic *currentGraphic = graphicOf(_curAnimateObject);
if (currentGraphic == nullptr && _isWalking)
currentGraphic = &_graphicNormal;
if (currentGraphic == nullptr && g_engine->world().somebodyUsing(this)) {
currentGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
currentGraphic->start(true);
currentGraphic->pause();
}
if (currentGraphic == nullptr) {
// The original game drew the current dialog line at this point,
// but I do not know of a scenario where this would be necessary
// As long as we cannot test this or have a bug report I rather not implement it
currentGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
}
assert(currentGraphic != nullptr);
g_engine->drawQueue().add(*currentGraphic, true, BlendMode::AdditiveAlpha, _lodBias);
}
void WalkingCharacter::drawDebug() {
Character::drawDebug();
auto renderer = dynamic_cast(&g_engine->renderer());
if (!g_engine->console().showCharacters() || renderer == nullptr || !isEnabled() || _pathPoints.empty())
return;
Array points2D(_pathPoints.size() + 1);
_pathPoints.push(_sourcePos);
for (uint i = 0; i < _pathPoints.size(); i++) {
auto v = g_engine->camera().transform3Dto2D({ (float)_pathPoints[i].x, (float)_pathPoints[i].y, kBaseScale });
points2D[i] = { v.x(), v.y() };
}
_pathPoints.pop();
renderer->debugPolyline({ points2D.data(), points2D.size() }, kWhite);
}
void WalkingCharacter::loadResources() {
Character::loadResources();
for (int i = 0; i < kDirectionCount; i++) {
_walkingAnimations[i]->load();
_talkingAnimations[i]->load();
}
}
void WalkingCharacter::freeResources() {
Character::freeResources();
for (int i = 0; i < kDirectionCount; i++) {
_walkingAnimations[i]->freeImages();
_talkingAnimations[i]->freeImages();
}
}
void WalkingCharacter::syncGame(Serializer &serializer) {
Character::syncGame(serializer);
serializer.syncAsSint32LE(_lastWalkAnimFrame);
serializer.syncAsSint32LE(_walkedDistance);
syncPoint(serializer, _sourcePos);
syncPoint(serializer, _currentPos);
serializer.syncAsByte(_isWalking);
syncStack(serializer, _pathPoints, syncPoint);
syncEnum(serializer, _direction);
}
struct ArriveTask final : public Task {
ArriveTask(Process &process, const WalkingCharacter *character)
: Task(process)
, _character(character) {}
ArriveTask(Process &process, Serializer &s)
: Task(process) {
ArriveTask::syncGame(s);
}
TaskReturn run() override {
return _character->isWalking()
? TaskReturn::yield()
: TaskReturn::finish(1);
}
void debugPrint() override {
g_engine->getDebugger()->debugPrintf("Wait for %s to arrive", _character->name().c_str());
}
void syncGame(Serializer &s) override {
syncObjectAsString(s, _character);
}
const char *taskName() const override;
private:
const WalkingCharacter *_character = nullptr;
};
DECLARE_TASK(ArriveTask)
Task *WalkingCharacter::waitForArrival(Process &process) {
return new ArriveTask(process, this);
}
const char *MainCharacter::typeName() const { return "MainCharacter"; }
MainCharacter::MainCharacter(Room *room, ReadStream &stream)
: WalkingCharacter(room, stream)
, _semaphore(name().firstChar() == 'M' ? "mortadelo" : "filemon") {
stream.readByte(); // unused byte
_order = 100;
_kind =
name().equalsIgnoreCase("MORTADELO") ? MainCharacterKind::Mortadelo
: name().equalsIgnoreCase("FILEMON") ? MainCharacterKind::Filemon
: MainCharacterKind::None;
}
MainCharacter::~MainCharacter() {
for (auto *item : _items)
delete item;
}
bool MainCharacter::isBusy() const {
return !_semaphore.isReleased() || !g_engine->player().semaphore().isReleased();
}
void MainCharacter::update() {
if (_semaphore.isReleased())
_currentlyUsingObject = nullptr;
WalkingCharacter::update();
const int16 halfWidth = (int16)(60 * _graphicNormal.depthScale());
const int16 height = (int16)(310 * _graphicNormal.depthScale());
shape()->setAsRectangle(Rect(
_currentPos.x - halfWidth, _currentPos.y - height,
_currentPos.x + halfWidth, _currentPos.y));
// These are set as members as FloorColor might want to change them
_alphaPremultiplier = room()->characterAlphaPremultiplier();
_color = { 255, 255, 255, (uint8)(room()->characterAlphaTint() * 255 / 100) };
}
void MainCharacter::onArrived() {
if (_activateObject == nullptr)
return;
ITriggerableObject *activateObject = _activateObject;
const char *activateAction = _activateAction;
_activateObject = nullptr;
_activateAction = nullptr;
stopWalking(activateObject->interactionDirection());
if (g_engine->player().activeCharacter() == this)
activateObject->trigger(activateAction);
}
void MainCharacter::walkTo(
Point target_, Direction endDirection,
ITriggerableObject *activateObject, const char *activateAction) {
_activateObject = activateObject;
_activateAction = activateAction;
Point target = target_;
Point evadeTarget = target;
const PathFindingShape *activeFloor = room()->activeFloor();
if (activeFloor != nullptr && activeFloor->findPath(_currentPos, target, _pathPoints))
evadeTarget = _pathPoints[0];
MainCharacter *otherCharacter = &g_engine->world().getOtherMainCharacterByKind(_kind);
Point otherTarget = otherCharacter->_currentPos;
if (otherCharacter->isWalking() && !otherCharacter->_pathPoints.empty())
otherTarget = otherCharacter->_pathPoints[0];
const float activeDepthScale = g_engine->player().activeCharacter()->_graphicNormal.depthScale();
const float avoidanceDistSqr = pow(75 * activeDepthScale, 2);
const bool willIBeBusy =
_activateObject != nullptr &&
strcmp(_activateAction, "MIRAR") != 0 &&
otherCharacter->currentlyUsing() != dynamic_cast(_activateObject);
if (otherCharacter->room() == room() && evadeTarget.sqrDist(otherTarget) <= avoidanceDistSqr) {
if (!otherCharacter->isBusy()) {
if (activeFloor != nullptr && activeFloor->findEvadeTarget(evadeTarget, activeDepthScale, avoidanceDistSqr, evadeTarget))
otherCharacter->WalkingCharacter::walkTo(evadeTarget);
} else if (!willIBeBusy) {
if (activeFloor != nullptr)
activeFloor->findEvadeTarget(evadeTarget, activeDepthScale, avoidanceDistSqr, target);
}
}
WalkingCharacter::walkTo(target, endDirection, activateObject, activateAction);
if (this == g_engine->player().activeCharacter())
g_engine->camera().setFollow(this);
}
void MainCharacter::draw() {
if (this == &g_engine->world().mortadelo()) {
if (_currentPos.y <= g_engine->world().filemon()._currentPos.y) {
g_engine->world().mortadelo().drawInner();
g_engine->world().filemon().drawInner();
} else {
g_engine->world().filemon().drawInner();
g_engine->world().mortadelo().drawInner();
}
}
}
void MainCharacter::drawInner() {
if (room() != g_engine->player().currentRoom() || !isEnabled())
return;
Graphic *activeGraphic = graphicOf(_curAnimateObject);
if (activeGraphic == nullptr && _isWalking) {
activeGraphic = &_graphicNormal;
_graphicNormal.premultiplyAlpha() = _alphaPremultiplier;
}
if (activeGraphic == nullptr) {
activeGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
_graphicTalking.premultiplyAlpha() = _alphaPremultiplier;
}
assert(activeGraphic != nullptr);
activeGraphic->color() = _color;
g_engine->drawQueue().add(*activeGraphic, true, BlendMode::AdditiveAlpha, _lodBias);
}
void syncDialogMenuLine(Serializer &serializer, DialogMenuLine &line) {
serializer.syncAsSint32LE(line._dialogId);
serializer.syncAsSint32LE(line._yPosition);
serializer.syncAsSint32LE(line._returnValue);
}
void MainCharacter::syncGame(Serializer &serializer) {
String roomName = room()->name();
serializer.syncString(roomName);
if (serializer.isLoading()) {
room() = room()->world().getRoomByName(roomName.c_str());
if (room() == nullptr)
// no good way to recover from this
error("Invalid room name \"%s\" saved for \"%s\"", roomName.c_str(), name().c_str());
}
WalkingCharacter::syncGame(serializer);
FakeSemaphore::sync(serializer, _semaphore);
syncArray(serializer, _dialogLines, syncDialogMenuLine);
syncObjectAsString(serializer, _currentlyUsingObject);
for (auto *item : _items) {
bool isEnabled = item->isEnabled();
serializer.syncAsByte(isEnabled);
item->toggle(isEnabled);
}
}
void MainCharacter::clearInventory() {
for (auto *item : _items)
item->toggle(false);
if (g_engine->player().activeCharacter() == this)
g_engine->player().heldItem() = nullptr;
g_engine->world().inventory().updateItemsByActiveCharacter();
}
Item *MainCharacter::getItemByName(const String &name) const {
for (auto *item : _items) {
if (item->name() == name)
return item;
}
return nullptr;
}
bool MainCharacter::hasItem(const String &name) const {
auto item = getItemByName(name);
return item == nullptr || item->isEnabled();
}
void MainCharacter::pickup(const String &name, bool putInHand) {
auto item = getItemByName(name);
if (item == nullptr) {
g_engine->game().unknownPickupItem(name.c_str());
return;
}
item->toggle(true);
if (g_engine->player().activeCharacter() == this) {
if (putInHand)
g_engine->player().heldItem() = item;
g_engine->world().inventory().updateItemsByActiveCharacter();
}
}
void MainCharacter::drop(const Common::String &name) {
if (!name.empty()) {
auto item = getItemByName(name);
if (item == nullptr)
g_engine->game().unknownDropItem(name.c_str());
else
item->toggle(false);
}
if (g_engine->player().activeCharacter() == this) {
g_engine->player().heldItem() = nullptr;
g_engine->world().inventory().updateItemsByActiveCharacter();
}
}
void MainCharacter::walkToMouse() {
Point targetPos = g_engine->input().mousePos3D();
if (room()->activeFloor() != nullptr) {
// original would be overwriting the current path but this
// can cause the character teleporting to the new target
Stack tmpPath;
room()->activeFloor()->findPath(_sourcePos, targetPos, tmpPath);
if (!tmpPath.empty())
targetPos = tmpPath[0];
}
const uint minDistance = (uint)(50 * _graphicNormal.depthScale());
if (_sourcePos.sqrDist(targetPos) > minDistance * minDistance)
walkTo(targetPos);
}
bool MainCharacter::clearTargetIf(const ITriggerableObject *target) {
if (_activateObject == target) {
_activateObject = nullptr;
return true;
}
return false;
}
struct DialogMenuTask final : public Task {
DialogMenuTask(Process &process, MainCharacter *character)
: Task(process)
, _input(g_engine->input())
, _character(character) {}
DialogMenuTask(Process &process, Serializer &s)
: Task(process)
, _input(g_engine->input()) {
DialogMenuTask::syncGame(s);
}
TaskReturn run() override {
TASK_BEGIN;
layoutLines();
while (true) {
TASK_YIELD(1);
if (g_engine->player().activeCharacter() != _character) //-V779
continue;
g_engine->globalUI().updateChangingCharacter();
g_engine->player().heldItem() = nullptr;
g_engine->player().drawCursor();
_clickedLineI = updateLines();
if (_clickedLineI != UINT_MAX) {
TASK_YIELD(2);
TASK_WAIT(3, _character->sayText(process(), _character->_dialogLines[_clickedLineI]._dialogId));
int32 returnValue = _character->_dialogLines[_clickedLineI]._returnValue;
_character->_dialogLines.clear();
TASK_RETURN(returnValue);
}
}
TASK_END;
}
void debugPrint() override {
g_engine->console().debugPrintf("DialogMenu for %s with %u lines\n",
_character->name().c_str(), _character->_dialogLines.size());
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
syncObjectAsString(s, _character);
s.syncAsUint32LE(_clickedLineI);
}
const char *taskName() const override;
private:
static constexpr int kTextXOffset = 5;
static constexpr int kTextYOffset = 10;
inline int maxTextWidth() const {
return g_system->getWidth() - 2 * kTextXOffset;
}
void layoutLines() {
auto &lines = _character->_dialogLines;
for (auto &itLine : lines) {
// we reuse the draw request to measure the actual height without using it to actually draw
TextDrawRequest request(
g_engine->globalUI().dialogFont(),
g_engine->world().getDialogLine(itLine._dialogId),
Point(kTextXOffset, 0), maxTextWidth(), false, kWhite, 2);
itLine._yPosition = request.size().y; // briefly storing line height
}
lines.back()._yPosition = g_system->getHeight() - kTextYOffset - lines.back()._yPosition;
for (uint i = lines.size() - 1; i > 0; i--)
lines[i - 1]._yPosition = lines[i]._yPosition - kTextYOffset - lines[i - 1]._yPosition;
}
uint updateLines() {
bool isSomethingHovered = false;
for (uint i = _character->_dialogLines.size(); i > 0; i--) {
auto &itLine = _character->_dialogLines[i - 1];
bool isHovered = !isSomethingHovered && _input.mousePos2D().y >= itLine._yPosition - kTextYOffset;
g_engine->drawQueue().add(
g_engine->globalUI().dialogFont(),
g_engine->world().getDialogLine(itLine._dialogId),
Point(kTextXOffset, itLine._yPosition),
maxTextWidth(), false, isHovered ? Color { 255, 255, 128, 255 } : kWhite, -kForegroundOrderCount + 2);
isSomethingHovered = isSomethingHovered || isHovered;
if (isHovered && _input.wasMouseLeftReleased())
return i - 1;
}
return UINT_MAX;
}
Input &_input;
MainCharacter *_character = nullptr;
uint _clickedLineI = UINT_MAX;
};
DECLARE_TASK(DialogMenuTask)
void MainCharacter::addDialogLine(int32 dialogId) {
assert(dialogId >= 0);
DialogMenuLine line;
line._dialogId = dialogId;
_dialogLines.push_back(line);
}
void MainCharacter::setLastDialogReturnValue(int32 returnValue) {
if (_dialogLines.empty())
error("Tried to set return value of non-existent dialog line");
_dialogLines.back()._returnValue = returnValue;
}
Task *MainCharacter::dialogMenu(Process &process) {
if (_dialogLines.empty())
error("Tried to open dialog menu without any lines set");
return new DialogMenuTask(process, this);
}
void MainCharacter::resetUsingObjectAndDialogMenu() {
_currentlyUsingObject = nullptr;
_dialogLines.clear();
}
const char *Background::typeName() const { return "Background"; }
Background::Background(Room *room, const String &animationFileName, int16 scale)
: GraphicObject(room, "BACKGROUND") {
toggle(true);
_graphic.setAnimation(animationFileName, AnimationFolder::Backgrounds);
_graphic.scale() = scale;
_graphic.order() = 59;
}
const char *FloorColor::typeName() const { return "FloorColor"; }
FloorColor::FloorColor(Room *room, ReadStream &stream)
: ObjectBase(room, stream)
, _shape(stream) {}
void FloorColor::update() {
auto updateFor = [&] (MainCharacter &character) {
if (character.room() == room()) {
const auto result = _shape.colorAt(character.position());
if (result.first)
character.color() = { 255, 255, 255, result.second.a };
}
};
updateFor(g_engine->world().mortadelo());
updateFor(g_engine->world().filemon());
}
void FloorColor::drawDebug() {
auto renderer = dynamic_cast(&g_engine->renderer());
if (!g_engine->console().showFloorColor() || renderer == nullptr || !isEnabled())
return;
renderer->debugShape(*shape(), kDebugGreen);
}
Shape *FloorColor::shape() {
return &_shape;
}
}