/* 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "bladerunner/actor.h" #include "bladerunner/actor_clues.h" #include "bladerunner/actor_combat.h" #include "bladerunner/actor_walk.h" #include "bladerunner/audio_speech.h" #include "bladerunner/bladerunner.h" #include "bladerunner/boundingbox.h" #include "bladerunner/crimes_database.h" #include "bladerunner/game_info.h" #include "bladerunner/items.h" #include "bladerunner/mouse.h" #include "bladerunner/movement_track.h" #include "bladerunner/savefile.h" #include "bladerunner/scene.h" #include "bladerunner/scene_objects.h" #include "bladerunner/script/scene_script.h" #include "bladerunner/script/ai_script.h" #include "bladerunner/set.h" #include "bladerunner/slice_animations.h" #include "bladerunner/slice_renderer.h" #include "bladerunner/time.h" #include "bladerunner/subtitles.h" #include "bladerunner/waypoints.h" #include "bladerunner/zbuffer.h" namespace BladeRunner { Actor::Actor(BladeRunnerEngine *vm, int actorId) { _vm = vm; _id = actorId; _walkInfo = new ActorWalk(vm); _movementTrack = new MovementTrack(); _cluesLimit = (actorId == kActorMcCoy || actorId == kActorVoiceOver) ? 4 : 2; _clues = new ActorClues(vm, _cluesLimit); _combatInfo = new ActorCombat(vm); _friendlinessToOther.resize(_vm->_gameInfo->getActorCount()); setup(actorId); } Actor::~Actor() { delete _combatInfo; delete _clues; delete _movementTrack; delete _walkInfo; } void Actor::setup(int actorId) { _id = actorId; _setId = -1; _position = Vector3(0.0, 0.0, 0.0); _facing = 512; _targetFacing = -1; _walkboxId = -1; _animationId = 0; _animationFrame = 0; _fps = 15; _frameMs = 1000 / _fps; _mustReachWalkDestination = false; // Original's _inWalkLoop. Moved here from our constructor, since it's here in the original's init() _isMoving = false; _isTarget = false; _inCombat = false; _isInvisible = false; _isImmuneToObstacles = false; _isRetired = false; _width = 0; _height = 0; _retiredWidth = 0; _retiredHeight = 0; _scale = 1.0f; _timer4RemainDefault = 60000; _movementTrackWalkingToWaypointId = -1; _movementTrackDelayOnNextWaypoint = -1; for (int i = 0; i != kActorTimers; ++i) { _timersLeft[i] = 0; _timersLast[i] = _vm->_time->current(); } // This was in original code. // We need to init this timer in order to kick off periodic updates for acquireCluesByRelations _timersLeft[kActorTimerClueExchange] = _timer4RemainDefault; _honesty = 50; _intelligence = 50; _combatAggressiveness = 50; _stability = 50; _currentHP = 50; _maxHP = 50; _damageAnimIfMoving = true; // Set to true (like in original). And moved here from our constructor, since it's here in the original's init(). _goalNumber = -1; _movementTrackPaused = false; _movementTrackNextWaypointId = -1; _movementTrackNextDelay = -1; _movementTrackNextAngle = -1; _movementTrackNextRunning = false; _animationMode = -1; _screenRectangle = Common::Rect(-1, -1, -1, -1); _animationModeCombatIdle = kAnimationModeCombatIdle; _animationModeCombatWalk = kAnimationModeCombatWalk; _animationModeCombatRun = kAnimationModeCombatRun; int actorCount = (int)_vm->_gameInfo->getActorCount(); for (int i = 0; i != actorCount; ++i) _friendlinessToOther[i] = 50; #if BLADERUNNER_ORIGINAL_BUGS #else // if player actor was not idle and had an active _walkInfo then // upon starting a new game, the player actor would be put on the old _walkInfo _walkInfo->reset(); // // delete _walkInfo and re-allocate it (a reset method would probably be better) // if (_walkInfo != nullptr) { // delete(_walkInfo); // } // _walkInfo = new ActorWalk(_vm); #endif // BLADERUNNER_ORIGINAL_BUGS _combatInfo->setup(); _clues->removeAll(); _movementTrack->flush(); _actorSpeed = Vector3(); switch (_id) { case kActorMcCoy: _sitcomRatio = 50; break; case kActorGordo: _sitcomRatio = 0; break; case kActorGuzza: case kActorChew: case kActorVoiceOver: _sitcomRatio = 75; break; case kActorCrazylegs: case kActorBulletBob: case kActorRunciter: case kActorZuben: case kActorLeon: _sitcomRatio = 90; break; case kActorGrigorian: case kActorMoraji: _sitcomRatio = 100; break; default: _sitcomRatio = 33; break; } } void Actor::changeAnimationMode(int animationMode, bool force) { if (force) { _animationMode = -1; } if (animationMode != _animationMode) { _vm->_aiScripts->changeAnimationMode(_id, animationMode); _animationMode = animationMode; } } int Actor::getFPS() const { return _fps; } void Actor::setFPS(int fps) { _fps = fps; if (fps == 0) { // stop actor's animation _frameMs = 0; } else if (fps == -1) { // sync actor's animation with scene animation _frameMs = -1000; } else if (fps == -2) { // set FPS to default from the model _fps = _vm->_sliceAnimations->getFPS(_animationId); _frameMs = 1000 / _fps; } else { _frameMs = 1000 / fps; } } void Actor::increaseFPS() { #if BLADERUNNER_ORIGINAL_BUGS int fps = MIN(_fps + 3, 30); setFPS(fps); #else int oldFps = _fps; // new aux variable int fps = MIN(_fps + 3, 30); setFPS(fps); // Note: When stamina drain is disabled, McCoy returns to normal fps // (which is the default for his animations ie. 15 fps) // on his actor->tick() method, when he switches from running to walking animation // and setFPS(-2) is called if (!_vm->_disableStaminaDrain) { // Only McCoy is using the stamina timer in the game if (_id == kActorMcCoy) { if (_vm->_cutContent) { if (_fps > 20 && oldFps < _fps) { // only start the stamina timer // when McCOy's fps are more than 20 fps and purely increased // and only if the new drain interval is smaller than the previous one // the start drain interval is supposed to be slow // starting from 10 seconds and decreasing as low as 1 second // (It will barely come into play basically) int nextStaminaDrainInterval = (31 - _fps) * 1000; if (nextStaminaDrainInterval < timerLeft(kActorTimerRunningStaminaFPS)) { timerStart(kActorTimerRunningStaminaFPS, nextStaminaDrainInterval); } } } else { // just prevent any rogue state for stamina timer being 0 // at any time when McCoy's fps get increased if (timerLeft(kActorTimerRunningStaminaFPS) == 0) { timerStart(kActorTimerRunningStaminaFPS, 200); } } } } #endif // BLADERUNNER_ORIGINAL_BUGS } void Actor::timerStart(int timerId, int32 intervalMillis) { assert(timerId >= 0 && timerId < kActorTimers); _timersLeft[timerId] = intervalMillis; _timersLast[timerId] = _vm->_time->current(); } void Actor::timerReset(int timerId) { assert(timerId >= 0 && timerId < kActorTimers); _timersLeft[timerId] = 0; } // timerLeft can be negative - This is required for // the actor's animation update timer mostly (timer kActorTimerAnimationFrame) int32 Actor::timerLeft(int timerId) { assert(timerId >= 0 && timerId < kActorTimers); return _timersLeft[timerId]; } void Actor::timersUpdate() { for (int i = 0; i < kActorTimers; ++i) { timerUpdate(i); } } void Actor::timerUpdate(int timerId) { if (_timersLeft[timerId] == 0) { return; } uint32 timeNow = _vm->_time->current(); uint32 timeDiff = timeNow - _timersLast[timerId]; // unsigned difference is intentional _timersLast[timerId] = timeNow; _timersLeft[timerId] = _timersLeft[timerId] - timeDiff; if (_timersLeft[timerId] <= 0) { switch (timerId) { case kActorTimerAIScriptCustomTask0: // fall through case kActorTimerAIScriptCustomTask1: // fall through case kActorTimerAIScriptCustomTask2: if (!_vm->_aiScripts->isInsideScript() && !_vm->_sceneScript->isInsideScript()) { _vm->_aiScripts->timerExpired(_id, timerId); _timersLeft[timerId] = 0; } else { _timersLeft[timerId] = 1; } break; case kActorTimerMovementTrack: _timersLeft[kActorTimerMovementTrack] = 0; if (_movementTrack->isPaused()) { _timersLeft[kActorTimerMovementTrack] = 1; } else { movementTrackNext(false); } break; case kActorTimerClueExchange: // Exchange clues between actors acquireCluesByRelations(); _timersLeft[kActorTimerClueExchange] = _timer4RemainDefault; break; case kActorTimerAnimationFrame: // Actor animation frame timer break; case kActorTimerRunningStaminaFPS: // If stamina drain is disabled then // the timer will become zero and won't get initialized again // This is better than entirely skipping updating this specific timer // which would include constantly checking for it in a frequently repeated loop // If stamina drain is re-enabled, the timer will get initialized // either: // Vanilla mode: when McCoy starts running, // or if starting new game // or if loading a game where the timer was stored as 0 // Restored Content mode: when McCoy starts running fast enough if (!_vm->_disableStaminaDrain) { if (isRunning()) { if (_fps > 15) { int newFps = _fps - 2; if (newFps < 15) { newFps = 15; } setFPS(newFps); } } #if BLADERUNNER_ORIGINAL_BUGS _timersLeft[kActorTimerRunningStaminaFPS] = 200; #else if (_vm->_cutContent) { if (isRunning()) { // drain faster if closer to max fps (30), else slower _timersLeft[kActorTimerRunningStaminaFPS] = (31 - _fps) * 200; } else { // not running - stop the timer timerReset(kActorTimerRunningStaminaFPS); } } else { _timersLeft[kActorTimerRunningStaminaFPS] = 200; } #endif // BLADERUNNER_ORIGINAL_BUGS } break; default: break; } } } void Actor::movementTrackNext(bool omitAiScript) { bool hasNextMovement; bool running; int angle; int32 delayMillis; int waypointId; Vector3 waypointPosition; bool arrived; hasNextMovement = _movementTrack->next(&waypointId, &delayMillis, &angle, &running); _movementTrackNextWaypointId = waypointId; _movementTrackNextDelay = delayMillis; _movementTrackNextAngle = angle; _movementTrackNextRunning = running; if (hasNextMovement) { if (angle == -1) { angle = 0; } int waypointSetId = _vm->_waypoints->getSetId(waypointId); _vm->_waypoints->getXYZ(waypointId, &waypointPosition.x, &waypointPosition.y, &waypointPosition.z); if (_setId == waypointSetId && waypointSetId == _vm->_actors[0]->_setId) { // if target waypointSetId is in same set as both the actor and McCoy then call movementTrackWaypointReached stopWalking(false); _walkInfo->setup(_id, running, _position, waypointPosition, false, &arrived); _movementTrackWalkingToWaypointId = waypointId; _movementTrackDelayOnNextWaypoint = delayMillis; if (arrived) { movementTrackWaypointReached(); } } else { // teleport to target waypoint's set and position anyway // and schedule next movementTrackNext() using the kActorTimerMovementTrack setSetId(waypointSetId); setAtXYZ(waypointPosition, angle, true, false, false); if (!delayMillis) { delayMillis = 1; } if (delayMillis > 1) { changeAnimationMode(kAnimationModeIdle, false); } timerStart(kActorTimerMovementTrack, delayMillis); } //return true; } else { if (!omitAiScript) { _vm->_aiScripts->completedMovementTrack(_id); } //return false; } } void Actor::movementTrackPause() { _movementTrack->pause(); if (isWalking()) { _movementTrackPaused = true; stopWalking(false); } else { _movementTrackPaused = false; } } void Actor::movementTrackUnpause() { Vector3 waypointPosition; bool arrived; _movementTrack->unpause(); if (_movementTrackNextWaypointId >= 0 && _movementTrackPaused) { _vm->_waypoints->getXYZ(_movementTrackNextWaypointId, &waypointPosition.x, &waypointPosition.y, &waypointPosition.z); _walkInfo->setup(_id, _movementTrackNextRunning, _position, waypointPosition, false, &arrived); _movementTrackPaused = false; } } void Actor::movementTrackWaypointReached() { if (!_movementTrack->isPaused() && _id != kActorMcCoy) { if (_movementTrackWalkingToWaypointId >= 0 && _movementTrackDelayOnNextWaypoint >= 0) { #if !BLADERUNNER_ORIGINAL_BUGS Vector3 waypointPosition; int waypointSetId = _vm->_waypoints->getSetId(_movementTrackWalkingToWaypointId); _vm->_waypoints->getXYZ(_movementTrackWalkingToWaypointId, &waypointPosition.x, &waypointPosition.y, &waypointPosition.z); if (_setId != waypointSetId || waypointSetId != _vm->_actors[0]->_setId) { // teleport to target waypoint's set and position anyway // Code similar to movementTrackNext() setSetId(waypointSetId); if (_movementTrackNextAngle == -1) { _movementTrackNextAngle = 0; } setAtXYZ(waypointPosition, _movementTrackNextAngle, true, false, false); } else { // Honor the heading defined by the AI_Movement_Track_Append_With_Facing method if (_movementTrackNextAngle >= 0) { faceHeading(_movementTrackNextAngle, true); } } #endif if (!_movementTrackDelayOnNextWaypoint) { _movementTrackDelayOnNextWaypoint = 1; } if (_vm->_aiScripts->reachedMovementTrackWaypoint(_id, _movementTrackWalkingToWaypointId)) { // schedule next movementTrackNext() using the kActorTimerMovementTrack int32 delay = _movementTrackDelayOnNextWaypoint; if (delay > 1) { changeAnimationMode(kAnimationModeIdle, false); delay = _movementTrackDelayOnNextWaypoint; // todo: analyze if movement is changed in some aiscript->ChangeAnimationMode? } timerStart(kActorTimerMovementTrack, delay); } } _movementTrackWalkingToWaypointId = -1; _movementTrackDelayOnNextWaypoint = 0; } } void Actor::setAtXYZ(const Vector3 &position, int facing, bool snapFacing, bool moving, bool retired) { _position = position; setFacing(facing, snapFacing); if (_vm->_scene->getSetId() == _setId) { _walkboxId = _vm->_scene->_set->findWalkbox(_position.x, _position.y); } else { _walkboxId = -1; } setBoundingBox(_position, retired); _vm->_sceneObjects->remove(_id + kSceneObjectOffsetActors); if (_vm->_scene->getSetId() == _setId) { _vm->_sceneObjects->addActor(_id + kSceneObjectOffsetActors, _bbox, _screenRectangle, true, moving, _isTarget, retired); } } void Actor::setAtWaypoint(int waypointId, int angle, bool moving, bool retired) { Vector3 waypointPosition; _vm->_waypoints->getXYZ(waypointId, &waypointPosition.x, &waypointPosition.y, &waypointPosition.z); setAtXYZ(waypointPosition, angle, true, moving, retired); } bool Actor::loopWalk(const Vector3 &destination, int proximity, bool interruptible, bool runFlag, const Vector3 &start, float targetWidth, float targetSize, bool mustReach, bool *isRunningFlag, bool async) { *isRunningFlag = false; if (proximity > 0) { float dist = distance(_position, destination); if (dist - targetSize <= proximity) { return false; } } if (mustReach && !async && _id != kActorMcCoy && proximity <= 24) { if (distance(_vm->_playerActor->_position, destination) <= 24.0f) { _vm->_playerActor->stepAway(destination, 48.0f); } } if (_id != kActorMcCoy) { interruptible = false; } Vector3 destinationX(destination); if (proximity > 0) { findNearestPosition(&destinationX, targetWidth, proximity, targetSize, _position, destination); } bool walking = walkTo(runFlag, destinationX, mustReach); if (async) { return false; } if (!walking && proximity > 0) { walking = walkTo(runFlag, destination, mustReach); } if (!walking) { faceXYZ(destination, false); return false; } if (_id != kActorMcCoy) { _vm->_mouse->disable(); } if (interruptible) { _vm->_isWalkingInterruptible = true; _vm->_interruptWalking = false; } else { _vm->playerLosesControl(); } if (mustReach) { _mustReachWalkDestination = true; } bool wasInterrupted = false; while (_walkInfo->isWalking() && _vm->_gameIsRunning) { if (_walkInfo->isRunning()) { *isRunningFlag = true; } _vm->gameTick(); if (_id == kActorMcCoy && interruptible && _vm->_interruptWalking) { stopWalking(false); wasInterrupted = true; } } if (mustReach) { _mustReachWalkDestination = false; } if (interruptible) { _vm->_isWalkingInterruptible = false; } else { _vm->playerGainsControl(); } #if BLADERUNNER_ORIGINAL_BUGS if (!wasInterrupted && proximity == 0 && !_vm->_playerActorIdle) { setAtXYZ(destination, _facing, true, false, false); } #else if (!wasInterrupted && proximity == 0 && (_id == kActorMcCoy && !_vm->_playerActorIdle) && !isRetired() ) { setAtXYZ(destination, _facing, true, false, false); } #endif // BLADERUNNER_ORIGINAL_BUGS if (_id != kActorMcCoy) { _vm->_mouse->enable(); } return wasInterrupted; } bool Actor::walkTo(bool runFlag, const Vector3 &destination, bool mustReach) { bool arrived; return _walkInfo->setup(_id, runFlag, _position, destination, mustReach, &arrived); } bool Actor::loopWalkToActor(int otherActorId, int proximity, int interruptible, bool runFlag, bool mustReach, bool *isRunningFlag) { return loopWalk(_vm->_actors[otherActorId]->_position, proximity, interruptible, runFlag, _position, 24.0f, 24.0f, mustReach, isRunningFlag, false); } bool Actor::loopWalkToItem(int itemId, int proximity, int interruptible, bool runFlag, bool mustReach, bool *isRunningFlag) { float x, y, z; int width, height; _vm->_items->getXYZ(itemId, &x, &y, &z); _vm->_items->getWidthHeight(itemId, &width, &height); Vector3 itemPosition(x, y, z); return loopWalk(itemPosition, proximity, interruptible, runFlag, _position, width, 24.0f, mustReach, isRunningFlag, false); } bool Actor::loopWalkToSceneObject(const Common::String &objectName, int proximity, bool interruptible, bool runFlag, bool mustReach, bool *isRunningFlag) { int sceneObject = _vm->_scene->_set->findObject(objectName); if (sceneObject < 0) { return true; } BoundingBox bbox; if (!_vm->_scene->_set->objectGetBoundingBox(sceneObject, &bbox)) { return true; } float x0, y0, z0, x1, y1, z1; bbox.getXYZ(&x0, &y0, &z0, &x1, &y1, &z1); float closestDistance = distance(_position.x, _position.z, x0, z0); float closestX = x0; float closestZ = z0; float d = distance(_position.x, _position.z, x1, z0); if (d < closestDistance) { closestX = x1; closestZ = z0; closestDistance = d; } d = distance(_position.x, _position.z, x1, z1); if (d < closestDistance) { closestX = x1; closestZ = z1; closestDistance = d; } d = distance(_position.x, _position.z, x0, z1); if (d < closestDistance) { closestX = x0; closestZ = z1; } bool inWalkbox; float y = _vm->_scene->_set->getAltitudeAtXZ(closestX, closestZ, &inWalkbox); Vector3 destination(closestX, y, closestZ); return loopWalk(destination, proximity, interruptible, runFlag, _position, 0.0f, 24.0f, mustReach, isRunningFlag, false); } bool Actor::loopWalkToWaypoint(int waypointId, int proximity, int interruptible, bool runFlag, bool mustReach, bool *isRunningFlag) { Vector3 waypointPosition; _vm->_waypoints->getXYZ(waypointId, &waypointPosition.x, &waypointPosition.y, &waypointPosition.z); return loopWalk(waypointPosition, proximity, interruptible, runFlag, _position, 0.0f, 24.0f, mustReach, isRunningFlag, false); } bool Actor::loopWalkToXYZ(const Vector3 &destination, int proximity, bool interruptible, bool runFlag, bool mustReach, bool *isRunningFlag) { return loopWalk(destination, proximity, interruptible, runFlag, _position, 0.0f, 24.0f, mustReach, isRunningFlag, false); } bool Actor::asyncWalkToWaypoint(int waypointId, int proximity, bool runFlag, bool mustReach) { bool running; Vector3 waypointPosition; _vm->_waypoints->getXYZ(waypointId, &waypointPosition.x, &waypointPosition.y, &waypointPosition.z); return loopWalk(waypointPosition, proximity, false, runFlag, _position, 0.0f, 24.0f, mustReach, &running, true); } void Actor::asyncWalkToXYZ(const Vector3 &destination, int proximity, bool runFlag, bool mustReach) { bool running; loopWalk(destination, proximity, false, runFlag, _position, 0.0f, 24.0f, mustReach, &running, true); } void Actor::run() { _walkInfo->run(_id); } bool Actor::tick(bool forceDraw, Common::Rect *screenRect) { int32 timeLeft = 0; bool needsUpdate = false; if (_fps > 0) { // Note that when (some?) actors are retired (eg. Zuben) // their _fps is still > 0 so they will periodically set needsUpdate to true in their tick() (here) // Also, the moment an actor is retired does not necessarily means their death animation finished playing // Their death animation may finish a while later. // Thus, until it finished, their screen rectangle will be likely changing at the draw() operation. // Typically at the end of a death animation, the actor keeps updating for the same frame // (ie the last of the death animation). At that point their screen rectangle won't change at the draw() operation. timerUpdate(kActorTimerAnimationFrame); timeLeft = timerLeft(kActorTimerAnimationFrame); needsUpdate = (timeLeft <= 0); } else if (_fps == 0) { needsUpdate = false; } else if (forceDraw) { needsUpdate = true; timeLeft = 0; } if (needsUpdate) { int newAnimation = 0, newFrame = 0; _vm->_aiScripts->updateAnimation(_id, &newAnimation, &newFrame); assert(newFrame >= 0); if (_animationId != newAnimation) { if (_fps != 0 && _fps != -1) { _animationId = newAnimation; setFPS(-2); } } _animationId = newAnimation; _animationFrame = newFrame; Vector3 positionChange = _vm->_sliceAnimations->getPositionChange(_animationId); float angleChange = _vm->_sliceAnimations->getFacingChange(_animationId); if (_id == kActorHysteriaPatron1) { positionChange.x = 0.0f; positionChange.y = 0.0f; positionChange.z = 0.0f; } if (isWalking()) { if (0.0f <= positionChange.y) { positionChange.y = -4.0f; } _targetFacing = -1; bool walkInterrupted = _walkInfo->tick(_id, -positionChange.y, _mustReachWalkDestination); Vector3 pos; int facing; _walkInfo->getCurrentPosition(_id, &pos, &facing); setAtXYZ(pos, facing, false, _isMoving, false); if (walkInterrupted) { _vm->_actors[_id]->changeAnimationMode(kAnimationModeIdle); movementTrackWaypointReached(); if (inCombat()) { changeAnimationMode(_animationModeCombatIdle, false); } else { changeAnimationMode(kAnimationModeIdle, false); } } } else { // actor is not walking / is idle if (angleChange != 0.0f) { int facingChange = angleChange * (512.0f / M_PI); if (facingChange != 0) { _facing = _facing - facingChange; while (_facing < 0) { _facing += 1024; } while (_facing >= 1024) { _facing -= 1024; } } } if (_vm->_cutContent) { // the following Generic Walker models don't have an animation Id that is idle // so we use a frame of their walking animation to show them as stopped // However, we also need to override the positionChange vector for their walking animation too if ( (_id == kActorGenwalkerA || _id == kActorGenwalkerB || _id == kActorGenwalkerC) && (_animationId == 436 || _animationId == 434 || _animationId == 435 || _animationId == 422 || _animationId == 423) ) { positionChange.x = 0.0f; positionChange.y = 0.0f; positionChange.z = 0.0f; } } if (0.0f != positionChange.x || 0.0f != positionChange.y || 0.0f != positionChange.z) { if (_actorSpeed.x != 0.0f) { positionChange.x = positionChange.x * _actorSpeed.x; } if (_actorSpeed.y != 0.0f) { positionChange.y = positionChange.y * _actorSpeed.y; } if (_actorSpeed.z != 0.0f) { positionChange.z = positionChange.z * _actorSpeed.z; } float sinx = _vm->_sinTable1024->at(_facing); float cosx = _vm->_cosTable1024->at(_facing); float originalX = _position.x; float originalY = _position.y; float originalZ = _position.z; // Yes, Z & Y are switched between world space and model space. X is also negated for some unknown reason (wrong dirertion for angles?) _position.x = _position.x - positionChange.x * cosx - positionChange.y * sinx; _position.z = _position.z - positionChange.x * sinx + positionChange.y * cosx; _position.y = _position.y + positionChange.z; if (_vm->_sceneObjects->existsOnXZ(_id + kSceneObjectOffsetActors, _position.x, _position.z, false, false) == 1 && !_isImmuneToObstacles) { _position.x = originalX; _position.y = originalY; _position.z = originalZ; } setAtXYZ(_position, _facing, true, _isMoving, _isRetired); } } } bool isVisible = false; if (!_isInvisible) { // draw() will set the new screenRect for the actor // based on the current animation frame // the new screenRect may be empty, in which case draw returns false (thus isVisible will be false then). isVisible = draw(screenRect); if (isVisible) { _screenRectangle = *screenRect; } } #if !BLADERUNNER_ORIGINAL_BUGS // For consistency we need to init the screen rectangle and bbox for the actor's *scene object* // in a new scene (since we also reset the screen rectangle at Scene::open()) // for the case of the actor not moving if (_vm->_scene->getSetId() == _setId && !_isInvisible && _vm->_sceneObjects->findById(_id + kSceneObjectOffsetActors) != -1) { if (_vm->_sceneObjects->isEmptyScreenRectangle(_id + kSceneObjectOffsetActors)) { if (isVisible) { Vector3 pos = getPosition(); int facing = getFacing(); setAtXYZ(pos, facing, true, _isMoving, _isRetired); } else { resetScreenRectangleAndBbox(); _vm->_sceneObjects->resetScreenRectangleAndBbox(_id + kSceneObjectOffsetActors); } } else if (_vm->_sceneObjects->compareScreenRectangle(_id + kSceneObjectOffsetActors, _screenRectangle) != 0) { if (isVisible) { // keep actor's _screenRectangle synched with sceneObject's actor's screen rectange // don't do a setAtXYZ here though _vm->_sceneObjects->synchScreenRectangle(_id + kSceneObjectOffsetActors, _screenRectangle); } else { resetScreenRectangleAndBbox(); _vm->_sceneObjects->resetScreenRectangleAndBbox(_id + kSceneObjectOffsetActors); } } } if ((_vm->_scene->getSetId() != _setId || _isInvisible || !isVisible) && !_screenRectangle.isEmpty() ) { resetScreenRectangleAndBbox(); if (_vm->_sceneObjects->findById(_id + kSceneObjectOffsetActors) != -1 && !_vm->_sceneObjects->isEmptyScreenRectangle(_id + kSceneObjectOffsetActors)) { _vm->_sceneObjects->resetScreenRectangleAndBbox(_id + kSceneObjectOffsetActors); } } #endif if (needsUpdate) { // timeLeft is supposed to be negative or 0 here in the original! int32 nextFrameTime = timeLeft + _frameMs; if (nextFrameTime <= 0) { nextFrameTime = 1; } timerStart(kActorTimerAnimationFrame, nextFrameTime); } if (_targetFacing >= 0) { if (_targetFacing == _facing) { _targetFacing = -1; } else { setFacing(_targetFacing, false); } } return isVisible; } void Actor::tickCombat() { if (_id != kActorMcCoy && !_isRetired && _inCombat) { _combatInfo->tick(); } } bool Actor::draw(Common::Rect *screenRect) { Vector3 drawPosition(_position.x, -_position.z, _position.y + 2.0); #if !BLADERUNNER_ORIGINAL_BUGS // In the original game, Moraji appears to be floating above the ground a bit if (_id == kActorMoraji && _setId == kSetDR01_DR02_DR04) { drawPosition.z -= 6.0f; } #endif float drawAngle = M_PI - _facing * (M_PI / 512.0f); float drawScale = _scale; if (_vm->_shortyMode) { drawScale = 0.7f; } _vm->_sliceRenderer->drawInWorld(_animationId, _animationFrame, drawPosition, drawAngle, drawScale, _vm->_surfaceFront, _vm->_zbuffer->getData()); _vm->_sliceRenderer->getScreenRectangle(screenRect, _animationId, _animationFrame, drawPosition, drawAngle, drawScale); return !screenRect->isEmpty(); } int Actor::getSetId() const { return _setId; } void Actor::setSetId(int setId) { if (_setId == setId) { return; } int i; // leaving _setId for setId if (_setId > 0) { for (i = 0; i < (int)_vm->_gameInfo->getActorCount(); ++i) { if (_vm->_actors[i]->_id != _id && _vm->_actors[i]->_setId == _setId) { _vm->_aiScripts->otherAgentExitedThisSet(i, _id); } } } // _setId updated to new (arrived in) setId _setId = setId; _vm->_aiScripts->enteredSet(_id, _setId); if (_setId > 0) { for (i = 0; i < (int)_vm->_gameInfo->getActorCount(); ++i) { if (_vm->_actors[i]->_id != _id && _vm->_actors[i]->_setId == _setId) { _vm->_aiScripts->otherAgentEnteredThisSet(i, _id); } } } } void Actor::setFacing(int facing, bool halfOrSet) { if (facing < 0 || facing >= 1024) { return; } if (halfOrSet) { _facing = facing; return; } int cw; int ccw; int offset; if (facing > _facing) { cw = facing - _facing; ccw = _facing + 1024 - facing; } else { ccw = _facing - facing; cw = facing + 1024 - _facing; } if (cw < ccw) { if (cw <= 32) { offset = cw; } else { offset = cw / 2; } } else { if (ccw <= 32) { offset = -ccw; } else { offset = -ccw / 2; } } _facing += offset; while (_facing < 0) { _facing += 1024; } while (_facing >= 1024) { _facing -= 1024; } } void Actor::setBoundingBox(const Vector3 &position, bool retired) { if (retired || _isRetired) { _bbox.setXYZ(position.x - (_retiredWidth / 2.0f), position.y, position.z - (_retiredWidth / 2.0f), position.x + (_retiredWidth / 2.0f), position.y + _retiredHeight, position.z + (_retiredWidth / 2.0f)); } else { _bbox.setXYZ(position.x - 12.0f, position.y + 6.0f, position.z - 12.0f, position.x + 12.0f, position.y + 72.0f, position.z + 12.0f); } } void Actor::resetScreenRectangleAndBbox() { _screenRectangle.left = -1; _screenRectangle.top = -1; _screenRectangle.right = -1; _screenRectangle.bottom = -1; _bbox.setXYZ(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); } float Actor::distanceFromView(View *view) const{ float xDist = _position.x - view->_cameraPosition.x; float zDist = _position.z + view->_cameraPosition.y; // y<->z is intentional, not a bug return sqrt(xDist * xDist + zDist * zDist); } bool Actor::isWalking() const { return _walkInfo->isWalking(); } bool Actor::isRunning() const { return _walkInfo->isRunning(); } void Actor::stopWalking(bool value) { if (value && _id == kActorMcCoy) { _vm->_playerActorIdle = true; } if (isWalking()) { _walkInfo->stop(_id, true, _animationModeCombatIdle, kAnimationModeIdle); } else if (inCombat()) { changeAnimationMode(_animationModeCombatIdle, false); } else { changeAnimationMode(kAnimationModeIdle, false); } } void Actor::faceActor(int otherActorId, bool animate) { if (_setId != _vm->_scene->getSetId()) { return; } Actor *otherActor = _vm->_actors[otherActorId]; if (_setId != otherActor->_setId) { return; } faceXYZ(otherActor->_position, animate); } void Actor::faceObject(const Common::String &objectName, bool animate) { int objectId = _vm->_scene->findObject(objectName); if (objectId == -1) { return; } BoundingBox boundingBox; _vm->_scene->objectGetBoundingBox(objectId, &boundingBox); float x0, y0, z0, x1, y1, z1; boundingBox.getXYZ(&x0, &y0, &z0, &x1, &y1, &z1); float x = (x1 + x0) / 2.0f; float z = (z1 + z0) / 2.0f; faceXYZ(x, y0, z, animate); } void Actor::faceItem(int itemId, bool animate) { float x, y, z; _vm->_items->getXYZ(itemId, &x, &y, &z); faceXYZ(x, y, z, animate); } void Actor::faceWaypoint(int waypointId, bool animate) { float x, y, z; _vm->_waypoints->getXYZ(waypointId, &x, &y, &z); faceXYZ(x, y, z, animate); } void Actor::faceXYZ(float x, float y, float z, bool animate) { if (isWalking()) { stopWalking(false); } if (x == _position.x && z == _position.z) { return; } int heading = angle_1024(_position.x, _position.z, x, z); faceHeading(heading, animate); } void Actor::faceXYZ(const Vector3 &pos, bool animate) { faceXYZ(pos.x, pos.y, pos.z, animate); } void Actor::faceCurrentCamera(bool animate) { faceXYZ(_vm->_view->_cameraPosition.x, _vm->_view->_cameraPosition.z, -_vm->_view->_cameraPosition.y, animate); // y<->z is intentional, not a bug } void Actor::faceHeading(int heading, bool animate) { if (heading != _facing) { if (animate) { _targetFacing = heading; } else { setFacing(heading, true); } } } void Actor::modifyFriendlinessToOther(int otherActorId, signed int change) { _friendlinessToOther[otherActorId] = CLIP(_friendlinessToOther[otherActorId] + change, 0, 100); } void Actor::setFriendlinessToOther(int otherActorId, int friendliness) { _friendlinessToOther[otherActorId] = CLIP(friendliness, 0, 100); } bool Actor::checkFriendlinessAndHonesty(int otherActorId) { int honestyDiff = 2 * _friendlinessToOther[otherActorId] - _honesty; uint friendlinessRange; if (honestyDiff > 30) { friendlinessRange = 100; } else if (honestyDiff >= 0 && honestyDiff <= 30) { friendlinessRange = 50; } else { friendlinessRange = 0; } return _vm->_rnd.getRandomNumberRng(1, 100) <= friendlinessRange; } void Actor::setHonesty(int honesty) { _honesty = CLIP(honesty, 0, 100); } void Actor::setIntelligence(int intelligence) { _intelligence = CLIP(intelligence, 0, 100); } void Actor::setStability(int stability) { _stability = CLIP(stability, 0, 100); } void Actor::setCombatAggressiveness(int combatAggressiveness) { _combatAggressiveness = CLIP(combatAggressiveness, 0, 100); } void Actor::setInvisible(bool isInvisible) { _isInvisible = isInvisible; } void Actor::setImmunityToObstacles(bool isImmune) { _isImmuneToObstacles = isImmune; } void Actor::modifyCombatAggressiveness(signed int change) { _combatAggressiveness = CLIP(_combatAggressiveness + change, 0, 100); } void Actor::modifyHonesty(signed int change) { _honesty = CLIP(_honesty + change, 0, 100); } void Actor::modifyIntelligence(signed int change) { _intelligence = CLIP(_intelligence + change, 0, 100); } void Actor::modifyStability(signed int change) { _stability = CLIP(_stability + change, 0, 100); } void Actor::setFlagDamageAnimIfMoving(bool value) { _damageAnimIfMoving = value; } bool Actor::getFlagDamageAnimIfMoving() const { return _damageAnimIfMoving; } int Actor::getSitcomRatio() const { return _sitcomRatio; } void Actor::retire(bool retired, int width, int height, int retiredByActorId) { _isRetired = retired; _retiredWidth = MAX(width, 0); _retiredHeight = MAX(height, 0); if (_id == kActorMcCoy && _isRetired) { _vm->playerLosesControl(); _vm->_playerDead = true; } if (_isRetired) { _vm->_aiScripts->retired(_id, retiredByActorId); } } void Actor::setTarget(bool target) { _isTarget = target; } void Actor::setCurrentHP(int hp) { _currentHP = CLIP(hp, 0, 100); if (hp > 0) { retire(false, 0, 0, -1); } } void Actor::setHealth(int hp, int maxHp) { if (hp > maxHp) { hp = maxHp; } _currentHP = CLIP(hp, 0, 100); _maxHP = CLIP(maxHp, 0, 100); if (hp > 0) { retire(false, 0, 0, -1); } } void Actor::modifyCurrentHP(signed int change) { _currentHP = CLIP(_currentHP + change, 0, 100); if (_currentHP > 0) { retire(false, 0, 0, -1); } } void Actor::modifyMaxHP(signed int change) { _maxHP = CLIP(_maxHP + change, 0, 100); } void Actor::combatModeOn(int initialState, bool rangedAttack, int enemyId, int waypointType, int animationModeCombatIdle, int animationModeCombatWalk, int animationModeCombatRun, int fleeRatio, int coverRatio, int attackRatio, int damage, int range, bool unstoppable) { _animationModeCombatIdle = animationModeCombatIdle; _animationModeCombatWalk = animationModeCombatWalk; _animationModeCombatRun = animationModeCombatRun; _inCombat = true; if (_id != kActorMcCoy && enemyId != -1) { _combatInfo->combatOn(_id, initialState, rangedAttack, enemyId, waypointType, fleeRatio, coverRatio, attackRatio, damage, range, unstoppable); } stopWalking(false); changeAnimationMode(_animationModeCombatIdle, false); for (int i = 0; i < (int)_vm->_gameInfo->getActorCount(); ++i) { Actor *otherActor = _vm->_actors[i]; if (i != _id && otherActor->_setId == _setId && !otherActor->_isRetired) { _vm->_aiScripts->otherAgentEnteredCombatMode(i, _id, true); } } } void Actor::combatModeOff() { if (_id != kActorMcCoy) { _combatInfo->combatOff(); } _inCombat = false; stopWalking(false); changeAnimationMode(kAnimationModeIdle, false); for (int i = 0; i < (int)_vm->_gameInfo->getActorCount(); ++i) { Actor *otherActor = _vm->_actors[i]; if (i != _id && otherActor->_setId == _setId && !otherActor->_isRetired) { _vm->_aiScripts->otherAgentEnteredCombatMode(i, _id, false); } } } float Actor::distanceFromActor(int otherActorId) { return (_position - _vm->_actors[otherActorId]->_position).length(); } int Actor::angleTo(const Vector3 &target) const { int angle = angle_1024(_position.x, _position.z, target.x, target.z) - _facing; if (angle < -512) { angle += 1024; } else if (angle > 512) { angle -= 1024; } return angle; } float Actor::getX() const { return _position.x; } float Actor::getY() const { return _position.y; } float Actor::getZ() const { return _position.z; } Vector3 Actor::getXYZ() const { return _position; } int Actor::getFacing() const { return _facing; } int Actor::getAnimationMode() const { return _animationMode; } int Actor::getAnimationId() const { return _animationId; } void Actor::setGoal(int goalNumber) { int oldGoalNumber = _goalNumber; _goalNumber = goalNumber; if (goalNumber == oldGoalNumber) { return; } _vm->_aiScripts->goalChanged(_id, oldGoalNumber, goalNumber); _vm->_sceneScript->actorChangedGoal(_id, goalNumber, oldGoalNumber, _vm->_scene->getSetId() == _setId); } int Actor::getGoal() const { return _goalNumber; } void Actor::speechPlay(int sentenceId, bool voiceOver) { Common::String name = Common::String::format( "%02d-%04d%s.AUD", _id, sentenceId, _vm->_languageCode.c_str()); int pan = 0; if (!voiceOver && _id != BladeRunnerEngine::kActorVoiceOver) { #if BLADERUNNER_ORIGINAL_BUGS Vector3 screenPosition = _vm->_view->calculateScreenPosition(_position); pan = (75 * (2 * CLIP(screenPosition.x, 0, 640) - 640)) / 640; // map [0..640] to [-75..75] #else // There are a few situations whereby // the actor is not actually in the set when speaking, // and the original code would result in audio playing // from a wrong balance point (bad pan value). // We capture these situations here and set the pan explicitly. // Mainly, these are: // tv news, machine voices (from PCs, Doors etc) // dispatch calls when used as actor speech and not as ambient sounds // phone calls (From Guzza, to Guzza, Lucy, Clovis, Dektora, Steele) // and other special cases, where the actor is not actually in the scene. // // pan:: map [0..640] to [-75..75] if ((_id == kActorNewscaster && sentenceId >= 0 && sentenceId <= 240) || (_id == kActorTyrell && sentenceId >= 430 && sentenceId <= 460) || (_id == kActorGuzza && sentenceId >= 1540 && sentenceId <= 1600) || (_id == kActorGovernorKolvig && sentenceId >= 80 && sentenceId <= 130)) { // MA04 TV // x: 149 --> pan: -41 // PS05 TV // x: 527 --> pan: 48 // These quotes only play in kSetMA04 and kSetPS05 pan = (_vm->_playerActor->getSetId() == kSetMA04) ? -41 : 48; } else if ((_id == kActorLucy && sentenceId >= 500 && sentenceId <= 640) || (_id == kActorClovis && sentenceId >= 310 && sentenceId <= 540) || (_id == kActorDektora && sentenceId >= 220 && sentenceId <= 490) || (_id == kActorSteele && sentenceId >= 680 && sentenceId <= 820) || (_id == kActorGuzza && sentenceId >= 0 && sentenceId <= 70)) { // MA04 phone // x: 351 --> pan: 7 // These quotes only play in kSetMA04 pan = 7; } else if (_id == kActorGuzza && sentenceId >= 1380 && sentenceId <= 1480) { // NR02 phone (Taffy's) // x: 300 --> pan: -5 // DR06 phone (Twin's Apartment) // x: 565 --> pan: 57 // These quotes only play in either kSetNR02 or kSetDR06 pan = (_vm->_playerActor->getSetId() == kSetNR02) ? -5 : 57; } else if (_id == kActorAnsweringMachine) { if (sentenceId == 0) { // kSetBB07 - Bradbury, Sebastian's Lab Computer (0) // x: 567 --> pan: 58 pan = 58; } else if (sentenceId >= 10 && sentenceId <= 50) { // kSetDR06 - Luther & Lance answering machine [10, 50] // x: 278 --> pan: -11 pan = -11; } else if (sentenceId == 60) { // kSetDR06 - Twin's Apartment // Restored Cut Content quote // (Twin's Lab has a door announcer -- as heard in the video intro of Act 4 too) // Pan will be at vidphone spot // x: 565 --> pan: 57 pan = 57; } else if (sentenceId >= 330 && sentenceId <= 370) { // Mainframe terminal - x: 500 --> pan: 42 // These quotes only play in kSetPS06 pan = 42; } // Default pan is already set to 0 (ie. center) // Includes Maze Scenes (kSetPS10_PS11_PS12_PS13) - quotes [280, 320] // Also ESPER, KIA, VK, Elevator (MA06) and Spinner. } else { Vector3 actorScreenPosition; switch (_id) { case kActorLance: // Lance does not have a model, but he is "attached" to his twin Luther actorScreenPosition = _vm->_view->calculateScreenPosition(_vm->_actors[kActorLuther]->getPosition()); break; case kActorDispatcher: // kActorDispatcher does not have a model, but should be "attached" to McCoy or Steele if (sentenceId >= 0 && sentenceId <= 40) { // Steele's radio actorScreenPosition = _vm->_view->calculateScreenPosition(_vm->_actors[kActorSteele]->getPosition()); } else { // McCoy's radio actorScreenPosition = _vm->_view->calculateScreenPosition(_vm->_playerActor->getPosition()); } break; case kActorOfficerLeary: // Voice from dispatcher is attached to McCoy (coming from his radio) if ((sentenceId >= 240 && sentenceId <= 450) || (sentenceId == 460 && _vm->_language == Common::DE_DEU) || (sentenceId >= 480 && sentenceId <= 530 && _vm->_language == Common::ES_ESP) || (sentenceId >= 520 && sentenceId <= 530 && _vm->_language == Common::IT_ITA) ) { // responding to dispatch actorScreenPosition = _vm->_view->calculateScreenPosition(_vm->_playerActor->getPosition()); } else { actorScreenPosition = _vm->_view->calculateScreenPosition(_position); } break; case kActorOfficerGrayford: // Voice from dispatcher is attached to McCoy (coming from his radio) if ((sentenceId >= 360 && sentenceId <= 450) || (sentenceId == 460 && _vm->_language == Common::DE_DEU) || (sentenceId >= 470 && sentenceId <= 550) || (sentenceId >= 560 && sentenceId <= 610 && _vm->_language == Common::ES_ESP) ) { // responding to dispatch actorScreenPosition = _vm->_view->calculateScreenPosition(_vm->_playerActor->getPosition()); } else { actorScreenPosition = _vm->_view->calculateScreenPosition(_position); } break; default: actorScreenPosition = _vm->_view->calculateScreenPosition(_position); } pan = (75 * (2 * CLIP(actorScreenPosition.x, 0, 640) - 640)) / 640; // map [0..640] to [-75..75] } // debug("actor: %d, pan: %d", _id, pan); #endif // BLADERUNNER_ORIGINAL_BUGS } _vm->_subtitles->loadInGameSubsText(_id, sentenceId); _vm->_subtitles->show(); _vm->_audioSpeech->playSpeech(name, pan); } void Actor::speechStop() { _vm->_subtitles->hide(); _vm->_audioSpeech->stopSpeech(); } bool Actor::isSpeeching() { return _vm->_audioSpeech->isPlaying(); } void Actor::addClueToDatabase(int clueId, int weight, bool clueAcquired, bool unknownFlag, int fromActorId) { _clues->add(_id, clueId, weight, clueAcquired, unknownFlag, fromActorId); } bool Actor::canAcquireClue(int clueId) const { return _clues->exists(clueId); } void Actor::acquireClue(int clueId, bool unknownFlag, int fromActorId) { bool hasAlready = hasClue(clueId); _clues->acquire(clueId, unknownFlag, fromActorId); if (!hasAlready) { _vm->_aiScripts->receivedClue(_id, clueId, fromActorId); } } void Actor::loseClue(int clueId) { _clues->lose(clueId); } bool Actor::hasClue(int clueId) const { return _clues->isAcquired(clueId); } // This method is used exclusively for transfers from and to Mainframe. // It copies clues from this actor (_id) to a target actor (actorId). // Keep in mind that actors other than McCoy can also transfer clues to Mainframe (eg. Steele) // or retrieve from Mainframe (eg. Klein) // see: ScriptBase::Actor_Clues_Transfer_New_From_Mainframe() // ScriptBase::Actor_Clues_Transfer_New_To_Mainframe() // In Restored Content it will skip transfering clues that are Intangible (default clue type) // since those clues do not actually show up in McCoy's KIA bool Actor::copyClues(int actorId) { bool newCluesAcquired = false; Actor *otherActor = _vm->_actors[actorId]; for (int i = 0; i < (int)_vm->_gameInfo->getClueCount(); ++i) { int clueId = i; if (hasClue(clueId) && !_clues->isPrivate(clueId) && (!_vm->_cutContent || _vm->_crimesDatabase->getAssetType(clueId) != kClueTypeIntangible) && otherActor->canAcquireClue(clueId) && !otherActor->hasClue(clueId)) { int fromActorId = _id; if (_id == BladeRunnerEngine::kActorVoiceOver) { fromActorId = _clues->getFromActorId(clueId); } if (_vm->_cutContent && ((_id == BladeRunnerEngine::kActorVoiceOver && actorId == kActorMcCoy) || (_id == kActorMcCoy && actorId == BladeRunnerEngine::kActorVoiceOver) )) { // when transfering a clue successfully between McCoy (playerActor) and Mainframe, // we mark it as such, since if McCoy later marks it as hidden (with Bob's KIA hack) // the player will have some indication that this clue is already on the mainframe. // Hence manually hiding it would be pointless. // This, however, cannot cover the case that someone else (eg. Steele) uploaded clues // to the Mainframe, which McCoy had not yet tried to download. // So, eg. if Steele uploaded clueA, and McCoy also somehow acquired clueA (without synching with Mainframe) // then McCoy's KIA won't "know" that the Mainframe also has this clue, until he interacts / synchs with Mainframe _vm->_playerActor->_clues->setSharedWithMainframe(clueId, true); } otherActor->acquireClue(clueId, false, fromActorId); newCluesAcquired = true; } else if (_vm->_cutContent && hasClue(clueId) && otherActor->hasClue(clueId) && _vm->_crimesDatabase->getAssetType(clueId) != kClueTypeIntangible && ((_id == BladeRunnerEngine::kActorVoiceOver && actorId == kActorMcCoy) || (_id == kActorMcCoy && actorId == BladeRunnerEngine::kActorVoiceOver) ) ) { // In Restored Content also mark clues that were not exchanged, because both parties already have them _vm->_playerActor->_clues->setSharedWithMainframe(clueId, true); } } return newCluesAcquired; } void Actor::acquireCluesByRelations() { if (_setId >= 0 && _setId != kSetFreeSlotG && _setId != _vm->_actors[0]->_setId) { for (int i = 0; i < (int)_vm->_gameInfo->getActorCount(); ++i) { if (i != _id && _vm->_actors[i]->_setId == _setId && i && _id && checkFriendlinessAndHonesty(i) && _vm->_actors[i]->checkFriendlinessAndHonesty(_id)) { _clues->acquireCluesByRelations(_id, i); } } } } int Actor::soundVolume() const { float dist = distanceFromView(_vm->_view); return (35 * CLIP(100 - (dist / 12), 0, 100)) / 100; // map [0..1200] to [35..0] } // overrideRange argument was added to allow for more accurate sound balance on occasion (if required) int Actor::soundPan(uint8 overrideRange) const { Vector3 screenPosition = _vm->_view->calculateScreenPosition(_position); // By default map [0..640] to [-overrideRange..overrideRange] (default range [-35..35]) CLIP(overrideRange, 35, 100); return (overrideRange * (2 * CLIP(screenPosition.x, 0, 640) - 640)) / 640; } bool Actor::isObstacleBetween(const Vector3 &target) { return _vm->_sceneObjects->isObstacleBetween(_position, target, -1); } int Actor::findTargetUnderMouse(BladeRunnerEngine *vm, int mouseX, int mouseY) { int setId = vm->_scene->getSetId(); for (int i = 0; i < (int)vm->_gameInfo->getActorCount(); ++i) { if (vm->_actors[i]->isTarget() && vm->_actors[i]->getSetId() == setId) { if (vm->_actors[i]->_screenRectangle.contains(mouseX, mouseY)) { return i; } } } return -1; } bool Actor::findEmptyPositionAround(const Vector3 &startPosition, const Vector3 &targetPosition, float size, Vector3 *emptyPosition) { emptyPosition->x = 0.0f; emptyPosition->y = 0.0f; emptyPosition->z = 0.0f; int facingLeft = angle_1024(targetPosition, startPosition); int facingRight = facingLeft; int facingLeftCounter = 0; int facingRightCounter = 0; while (true) { float rotatedX = targetPosition.x + size * _vm->_sinTable1024->at(facingLeft); float rotatedZ = targetPosition.z - size * _vm->_cosTable1024->at(facingLeft); if (!_walkInfo->isXYZOccupied(rotatedX, targetPosition.y, rotatedZ, _id)) { if (_vm->_scene->_set->findWalkbox(rotatedX, rotatedZ) >= 0) { emptyPosition->x = rotatedX; emptyPosition->y = targetPosition.y; emptyPosition->z = rotatedZ; return true; } } else { // looks like a bug as it might not find anything when there is no walkbox at this angle facingLeft += 20; #if BLADERUNNER_ORIGINAL_BUGS if (facingLeft > 1024) { facingLeft -= 1024; } #else // if facingLeft + 20 == 1024 then it could cause the assertion fault // in common/sinetables.cpp for SineTable::at(int index) -> assert((index >= 0) && (index < _nPoints)) if (facingLeft >= 1024) { facingLeft -= 1024; } #endif facingLeftCounter += 20; } rotatedX = size * _vm->_sinTable1024->at(facingRight) + targetPosition.x; rotatedZ = size * _vm->_cosTable1024->at(facingRight) + targetPosition.z; if (!_walkInfo->isXYZOccupied(rotatedX, targetPosition.y, rotatedZ, _id)) { if (_vm->_scene->_set->findWalkbox(rotatedX, rotatedZ) >= 0) { emptyPosition->x = rotatedX; emptyPosition->y = targetPosition.y; emptyPosition->z = rotatedZ; return true; } } else { // looks like a bug as it might not find anything when there is no walkbox at this angle facingRight -= 20; if (facingRight < 0) { facingRight += 1024; } facingRightCounter += 20; } if (facingLeftCounter > 1024 && facingRightCounter > 1024) { return false; } } } bool Actor::findNearestPosition(Vector3 *nearestPosition, float targetWidth, int proximity, float targetSize, const Vector3 &startPosition, const Vector3 &targetPosition) { nearestPosition->x = 0.0f; nearestPosition->y = 0.0f; nearestPosition->z = 0.0f; float size = proximity + targetSize * 0.5f + targetWidth * 0.5f; float distance = (startPosition - targetPosition).length() - targetWidth * 0.5f - targetSize * 0.5f; if (size < distance) { return findEmptyPositionAround(startPosition, targetPosition, size, nearestPosition); } else { *nearestPosition = targetPosition; return true; } } bool Actor::stepAway(const Vector3 &destination, float distance) { Vector3 out; bool running; if (_walkInfo->findEmptyPositionAround(_id, destination, distance, out)) { loopWalk(out, 0, false, false, _position, 0.0f, 24.0f, false, &running, false); return true; } return false; } void Actor::save(SaveFileWriteStream &f) { f.writeInt(_id); f.writeInt(_setId); f.writeVector3(_position); f.writeInt(_facing); f.writeInt(_targetFacing); f.writeInt(_timer4RemainDefault); f.writeInt(_honesty); f.writeInt(_intelligence); f.writeInt(_combatAggressiveness); f.writeInt(_stability); f.writeInt(_goalNumber); f.writeInt(_currentHP); f.writeInt(_maxHP); f.writeBool(_movementTrackPaused); f.writeInt(_movementTrackNextWaypointId); f.writeInt(_movementTrackNextDelay); f.writeInt(_movementTrackNextAngle); f.writeBool(_movementTrackNextRunning); f.writeInt(_cluesLimit); f.writeBool(_isMoving); f.writeBool(_isTarget); f.writeBool(_inCombat); f.writeBool(_isInvisible); f.writeBool(_isRetired); f.writeBool(_isImmuneToObstacles); f.writeInt(_animationMode); f.writeInt(_fps); f.writeInt(_frameMs); f.writeInt(_animationId); f.writeInt(_animationFrame); f.writeInt(_movementTrackWalkingToWaypointId); f.writeInt(_movementTrackDelayOnNextWaypoint); f.writeRect(_screenRectangle); f.writeInt(_retiredWidth); f.writeInt(_retiredHeight); f.writeInt(_damageAnimIfMoving); f.writeInt(0); f.writeInt(0); f.writeFloat(_scale); for (int i = 0; i < kActorTimers; ++i) { f.writeInt(_timersLeft[i]); } uint32 now = _vm->_time->getPauseStart(); for (int i = 0; i < kActorTimers; ++i) { // this effectively stores the next timeDiff to be applied to timer i (in timerUpdate) f.writeInt(now - _timersLast[i]); // Unsigned difference is intentional } int actorCount = _vm->_gameInfo->getActorCount(); for (int i = 0; i != actorCount; ++i) { f.writeInt(_friendlinessToOther[i]); } _clues->save(f); _movementTrack->save(f); _walkInfo->save(f); f.writeBoundingBox(_bbox, false); _combatInfo->save(f); f.writeInt(_animationModeCombatIdle); f.writeInt(_animationModeCombatWalk); f.writeInt(_animationModeCombatRun); } void Actor::load(SaveFileReadStream &f) { _id = f.readInt(); _setId = f.readInt(); _position = f.readVector3(); _facing = f.readInt(); _targetFacing = f.readInt(); _timer4RemainDefault = f.readUint32LE(); _honesty = f.readInt(); _intelligence = f.readInt(); _combatAggressiveness = f.readInt(); _stability = f.readInt(); _goalNumber = f.readInt(); _currentHP = f.readInt(); _maxHP = f.readInt(); _movementTrackPaused = f.readBool(); _movementTrackNextWaypointId = f.readInt(); _movementTrackNextDelay = f.readInt(); _movementTrackNextAngle = f.readInt(); _movementTrackNextRunning = f.readBool(); _cluesLimit = f.readInt(); _isMoving = f.readBool(); _isTarget = f.readBool(); _inCombat = f.readBool(); _isInvisible = f.readBool(); _isRetired = f.readBool(); _isImmuneToObstacles = f.readBool(); _animationMode = f.readInt(); _fps = f.readInt(); _frameMs = f.readInt(); _animationId = f.readInt(); _animationFrame = f.readInt(); _movementTrackWalkingToWaypointId = f.readInt(); _movementTrackDelayOnNextWaypoint = f.readInt(); _screenRectangle = f.readRect(); _retiredWidth = f.readInt(); _retiredHeight = f.readInt(); _damageAnimIfMoving = f.readInt(); f.skip(4); f.skip(4); _scale = f.readFloat(); for (int i = 0; i < kActorTimers; ++i) { _timersLeft[i] = (int32)f.readUint32LE(); } // Bugfix: Special initialization case for timer 4 (kActorTimerClueExchange) when its value is restored as 0 // This should be harmless, but will remedy any broken save-games where the timer 4 was saved as 0. if (_timersLeft[kActorTimerClueExchange] == 0) { _timersLeft[kActorTimerClueExchange] = _timer4RemainDefault; } // Bugfix: Similar to the above // Special initialization case for timer 6 (kActorTimerRunningStaminaFPS) when its value is restored as 0 // This was due to an original game bug and it concerns only McCoy (player's actor) // This should be harmless, but will remedy any broken save-games where the timer 6 was saved as 0. // Also, in restored content mode we allow this counter to be 0 and only start it // when McCoy's FPS get increased (starts running with sufficient speed), // so the fix is not needed for RC mode if (!_vm->_cutContent) { if (_id == kActorMcCoy && _timersLeft[kActorTimerRunningStaminaFPS] == 0) { _timersLeft[kActorTimerClueExchange] = 200; } } uint32 now = _vm->_time->getPauseStart(); for (int i = 0; i < kActorTimers; ++i) { _timersLast[i] = now - f.readUint32LE(); // we require an unsigned difference here, since _timersLast is essentially keeping time } int actorCount = _vm->_gameInfo->getActorCount(); for (int i = 0; i != actorCount; ++i) { _friendlinessToOther[i] = f.readInt(); } _clues->load(f); _movementTrack->load(f); _walkInfo->load(f); _bbox = f.readBoundingBox(false); _combatInfo->load(f); _animationModeCombatIdle = f.readInt(); _animationModeCombatWalk = f.readInt(); _animationModeCombatRun = f.readInt(); } } // End of namespace BladeRunner