mirror of
https://github.com/scummvm/scummvm.git
synced 2026-06-20 05:45:29 +00:00
This fixes a regression after db71d92 in case Character.Animate() called during idling, and Loop number assertion is done prior to view changing back to normal view, could cause game to error. Pick out "stop_character_idling" function that checks idle state and releases the locked idle view. From upstream 946fa71c9332cae98f31d8b7672a1fe7b76d46c5 Fix #15523
469 lines
15 KiB
C++
469 lines
15 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "ags/shared/ac/character_info.h"
|
|
#include "ags/shared/ac/common.h"
|
|
#include "ags/shared/ac/game_setup_struct.h"
|
|
#include "ags/engine/ac/character.h"
|
|
#include "ags/engine/ac/character_extras.h"
|
|
#include "ags/engine/ac/game_state.h"
|
|
#include "ags/engine/ac/global_character.h"
|
|
#include "ags/engine/ac/global_game.h"
|
|
#include "ags/engine/ac/math.h"
|
|
#include "ags/engine/ac/object.h"
|
|
#include "ags/engine/ac/view_frame.h"
|
|
#include "ags/engine/debugging/debug_log.h"
|
|
#include "ags/shared/game/room_struct.h"
|
|
#include "ags/engine/main/update.h"
|
|
#include "ags/engine/media/audio/audio_system.h"
|
|
#include "ags/ags.h"
|
|
#include "ags/globals.h"
|
|
|
|
namespace AGS3 {
|
|
|
|
using namespace AGS::Shared;
|
|
|
|
#define Random __Rand
|
|
|
|
int CharacterInfo::get_baseline() const {
|
|
if (baseline < 1)
|
|
return y;
|
|
return baseline;
|
|
}
|
|
|
|
int CharacterInfo::get_blocking_top() const {
|
|
if (blocking_height > 0)
|
|
return y - blocking_height / 2;
|
|
return y - 2;
|
|
}
|
|
|
|
int CharacterInfo::get_blocking_bottom() const {
|
|
// the blocking_bottom should be 1 less than the top + height
|
|
// since the code does <= checks on it rather than < checks
|
|
if (blocking_height > 0)
|
|
return (y + (blocking_height + 1) / 2) - 1;
|
|
return y + 3;
|
|
}
|
|
|
|
void CharacterInfo::FixupCurrentLoopAndFrame() {
|
|
// If current loop property exceeds number of loops,
|
|
// or if selected loop has no frames, then try select any first loop that has frames.
|
|
// NOTE: although this may seem like a weird solution to a problem,
|
|
// we do so for backwards compatibility; this approximately emulates older games behavior.
|
|
|
|
if (view >= 0 &&
|
|
(loop >= _GP(views)[view].numLoops || _GP(views)[view].loops[loop].numFrames == 0)) {
|
|
for (loop = 0;
|
|
(loop < _GP(views)[view].numLoops) && (_GP(views)[view].loops[loop].numFrames == 0); ++loop) {
|
|
}
|
|
if (loop == _GP(views)[view].numLoops) {
|
|
// view has no frames?!
|
|
// amazingly enough there are old games that allow this to happen...
|
|
if (_G(loaded_game_file_version) >= kGameVersion_300)
|
|
quitprintf("!Character %s is assigned view %d that has no frames!", scrname, view);
|
|
loop = 0;
|
|
}
|
|
}
|
|
|
|
// If the last saved frame exceeds a new loop, then switch to frame 1
|
|
// (first walking frame) if walking, or frame 0 otherwise or if there's less than 2 frames.
|
|
int frames_in_loop = _GP(views)[view].loops[loop].numFrames;
|
|
if (frame >= frames_in_loop) {
|
|
frame = (walking > 0 && frames_in_loop > 1) ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
void CharacterInfo::UpdateMoveAndAnim(int &char_index, CharacterExtras *chex, std::vector<int> &followingAsSheep) {
|
|
int res;
|
|
|
|
if (on != 1)
|
|
return;
|
|
|
|
// Turn around during walk
|
|
res = update_character_walkturning(chex);
|
|
// Fixup character's loop prior to any further logic updates
|
|
FixupCurrentLoopAndFrame();
|
|
|
|
// FIXME: refactor this nonsense!
|
|
// [IKM] Yes, it should return! upon getting RETURN_CONTINUE here
|
|
if (res == RETURN_CONTINUE) { // [IKM] now, this is one of those places...
|
|
return; // must be careful not to screw things up
|
|
}
|
|
|
|
int doing_nothing = 1;
|
|
update_character_moving(char_index, chex, doing_nothing);
|
|
|
|
// [IKM] 2012-06-28:
|
|
// Character index value is used to set up some variables in there, so I cannot just cease using it
|
|
res = update_character_animating(char_index, doing_nothing);
|
|
// [IKM] Yes, it should return! upon getting RETURN_CONTINUE here
|
|
if (res == RETURN_CONTINUE) { // [IKM] now, this is one of those places...
|
|
return; // must be careful not to screw things up
|
|
}
|
|
|
|
update_character_follower(char_index, followingAsSheep, doing_nothing);
|
|
|
|
update_character_idle(chex, doing_nothing);
|
|
|
|
chex->process_idle_this_time = 0;
|
|
}
|
|
|
|
void CharacterInfo::UpdateFollowingExactlyCharacter() {
|
|
x = _GP(game).chars[following].x;
|
|
y = _GP(game).chars[following].y;
|
|
z = _GP(game).chars[following].z;
|
|
room = _GP(game).chars[following].room;
|
|
prevroom = _GP(game).chars[following].prevroom;
|
|
|
|
int usebase = _GP(game).chars[following].get_baseline();
|
|
|
|
if (flags & CHF_BEHINDSHEPHERD)
|
|
baseline = usebase - 1;
|
|
else
|
|
baseline = usebase + 1;
|
|
}
|
|
|
|
int CharacterInfo::update_character_walkturning(CharacterExtras *chex) {
|
|
if (walking >= TURNING_AROUND) {
|
|
// Currently rotating to correct direction
|
|
if (walkwait > 0) walkwait--;
|
|
else {
|
|
// Work out which direction is next
|
|
int wantloop = find_looporder_index(loop) + 1;
|
|
// going anti-clockwise, take one before instead
|
|
if (walking >= TURNING_BACKWARDS)
|
|
wantloop -= 2;
|
|
while (1) {
|
|
if (wantloop >= 8)
|
|
wantloop = 0;
|
|
if (wantloop < 0)
|
|
wantloop = 7;
|
|
if ((turnlooporder[wantloop] >= _GP(views)[view].numLoops) ||
|
|
(_GP(views)[view].loops[turnlooporder[wantloop]].numFrames < 1) ||
|
|
((turnlooporder[wantloop] >= 4) && ((flags & CHF_NODIAGONAL) != 0))) {
|
|
if (walking >= TURNING_BACKWARDS)
|
|
wantloop--;
|
|
else
|
|
wantloop++;
|
|
} else break;
|
|
}
|
|
loop = turnlooporder[wantloop];
|
|
walking -= TURNING_AROUND;
|
|
// if still turning, wait for next frame
|
|
if (walking % TURNING_BACKWARDS >= TURNING_AROUND)
|
|
walkwait = animspeed;
|
|
else
|
|
walking = walking % TURNING_BACKWARDS;
|
|
chex->animwait = 0;
|
|
}
|
|
return RETURN_CONTINUE;
|
|
//continue;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void CharacterInfo::update_character_moving(int &char_index, CharacterExtras *chex, int &doing_nothing) {
|
|
if ((walking > 0) && (room == _G(displayed_room))) {
|
|
if (walkwait > 0) walkwait--;
|
|
else {
|
|
flags &= ~CHF_AWAITINGMOVE;
|
|
|
|
// Move the character
|
|
int numSteps = wantMoveNow(this, chex);
|
|
|
|
if ((numSteps) && (chex->xwas != INVALID_X)) {
|
|
// if the zoom level changed mid-move, the walkcounter
|
|
// might not have come round properly - so sort it out
|
|
x = chex->xwas;
|
|
y = chex->ywas;
|
|
chex->xwas = INVALID_X;
|
|
}
|
|
|
|
int oldxp = x, oldyp = y;
|
|
|
|
for (int ff = 0; ff < abs(numSteps); ff++) {
|
|
if (doNextCharMoveStep(this, char_index, chex))
|
|
break;
|
|
if ((walking == 0) || (walking >= TURNING_AROUND))
|
|
break;
|
|
}
|
|
|
|
if (numSteps < 0) {
|
|
// very small scaling, intersperse the movement
|
|
// to stop it being jumpy
|
|
chex->xwas = x;
|
|
chex->ywas = y;
|
|
x = ((x) - oldxp) / 2 + oldxp;
|
|
y = ((y) - oldyp) / 2 + oldyp;
|
|
} else if (numSteps > 0)
|
|
chex->xwas = INVALID_X;
|
|
|
|
if ((flags & CHF_ANTIGLIDE) == 0)
|
|
walkwaitcounter++;
|
|
}
|
|
|
|
// Fixup character's loop, it may be changed when making a walk-move
|
|
FixupCurrentLoopAndFrame();
|
|
|
|
doing_nothing = 0; // still walking?
|
|
|
|
if (walking < 1) {
|
|
// Finished walking, stop and reset state
|
|
chex->process_idle_this_time = 1;
|
|
doing_nothing = 1;
|
|
walkwait = 0;
|
|
Character_StopMoving(this);
|
|
if ((flags & CHF_MOVENOTWALK) == 0) {
|
|
// use standing pic
|
|
chex->animwait = 0;
|
|
frame = 0;
|
|
chex->CheckViewFrame(this);
|
|
}
|
|
} else if (chex->animwait > 0) {
|
|
chex->animwait--;
|
|
} else {
|
|
if (flags & CHF_ANTIGLIDE)
|
|
walkwaitcounter++;
|
|
|
|
if ((flags & CHF_MOVENOTWALK) == 0) {
|
|
frame++;
|
|
if (frame >= _GP(views)[view].loops[loop].numFrames) {
|
|
// end of loop, so loop back round skipping the standing frame
|
|
frame = 1;
|
|
|
|
if (_GP(views)[view].loops[loop].numFrames < 2)
|
|
frame = 0;
|
|
}
|
|
|
|
chex->animwait = _GP(views)[view].loops[loop].frames[frame].speed + animspeed;
|
|
|
|
if (flags & CHF_ANTIGLIDE)
|
|
walkwait = chex->animwait;
|
|
else
|
|
walkwait = 0;
|
|
|
|
chex->CheckViewFrame(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int CharacterInfo::update_character_animating(int &aa, int &doing_nothing) {
|
|
CharacterExtras *chex = &_GP(charextra)[index_id];
|
|
|
|
// not moving, but animating
|
|
// idleleft is <0 while idle view is playing (.animating is 0)
|
|
if (((animating != 0) || (idleleft < 0)) &&
|
|
((walking == 0) || ((flags & CHF_MOVENOTWALK) != 0)) &&
|
|
(room == _G(displayed_room))) {
|
|
doing_nothing = 0;
|
|
// idle anim doesn't count as doing something
|
|
if (idleleft < 0)
|
|
doing_nothing = 1;
|
|
|
|
if (wait > 0) wait--;
|
|
else if ((_G(char_speaking) == aa) && (_GP(game).options[OPT_LIPSYNCTEXT] != 0)) {
|
|
// currently talking with lip-sync speech
|
|
int fraa = frame;
|
|
wait = update_lip_sync(view, loop, &fraa) - 1;
|
|
// closed mouth at end of sentence
|
|
// NOTE: standard lip-sync is synchronized with text timer, not voice file
|
|
if (_GP(play).speech_in_post_state ||
|
|
((_GP(play).messagetime >= 0) && (_GP(play).messagetime < _GP(play).close_mouth_speech_time)))
|
|
frame = 0;
|
|
|
|
if (frame != fraa) {
|
|
frame = fraa;
|
|
chex->CheckViewFrame(this);
|
|
}
|
|
|
|
//continue;
|
|
return RETURN_CONTINUE;
|
|
} else {
|
|
// Normal view animation
|
|
const int oldframe = frame;
|
|
|
|
bool done_anim = false;
|
|
if ((aa == _G(char_speaking)) &&
|
|
(_GP(play).speech_in_post_state ||
|
|
((!_GP(play).speech_has_voice) &&
|
|
(_GP(play).close_mouth_speech_time > 0) &&
|
|
(_GP(play).messagetime < _GP(play).close_mouth_speech_time)))) {
|
|
// finished talking - stop animation
|
|
done_anim = true;
|
|
frame = 0;
|
|
} else {
|
|
if (!CycleViewAnim(view, loop, frame, get_anim_forwards(), get_anim_repeat())) {
|
|
done_anim = true; // finished animating
|
|
// end of idle anim
|
|
if (idleleft < 0) {
|
|
// constant anim, reset (need this cos animating==0)
|
|
if (idletime == 0)
|
|
frame = 0;
|
|
// one-off anim, stop
|
|
else {
|
|
ReleaseCharacterView(aa);
|
|
idleleft = idletime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
wait = _GP(views)[view].loops[loop].frames[frame].speed;
|
|
// idle anim doesn't have speed stored cos animating==0 (TODO: investigate why?)
|
|
if (idleleft < 0)
|
|
wait += idle_anim_speed;
|
|
else
|
|
wait += get_anim_delay();
|
|
|
|
if (frame != oldframe)
|
|
chex->CheckViewFrame(this);
|
|
|
|
if (done_anim)
|
|
stop_character_anim(this);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void CharacterInfo::update_character_follower(int &aa, std::vector<int> &followingAsSheep, int &doing_nothing) {
|
|
if ((following >= 0) && (followinfo == FOLLOW_ALWAYSONTOP)) {
|
|
// an always-on-top follow
|
|
followingAsSheep.push_back(aa);
|
|
}
|
|
|
|
// not moving, but should be following another character
|
|
else if ((following >= 0) && (doing_nothing == 1)) {
|
|
short distaway = (followinfo >> 8) & 0x00ff;
|
|
// no character in this room
|
|
if ((_GP(game).chars[following].on == 0) || (on == 0));
|
|
else if (room < 0) {
|
|
room++;
|
|
if (room == 0) {
|
|
// appear in the new room
|
|
room = _GP(game).chars[following].room;
|
|
x = _GP(play).entered_at_x;
|
|
y = _GP(play).entered_at_y;
|
|
}
|
|
}
|
|
// wait a bit, so we're not constantly walking
|
|
else if (Random(100) < (followinfo & 0x00ff));
|
|
// the followed character has changed room
|
|
else if ((room != _GP(game).chars[following].room)
|
|
&& (_GP(game).chars[following].on == 0))
|
|
; // do nothing if the player isn't visible
|
|
else if (room != _GP(game).chars[following].room) {
|
|
prevroom = room;
|
|
room = _GP(game).chars[following].room;
|
|
|
|
if (room == _G(displayed_room)) {
|
|
// only move to the room-entered position if coming into
|
|
// the current room
|
|
if (_GP(play).entered_at_x > (_GP(thisroom).Width - 8)) {
|
|
x = _GP(thisroom).Width + 8;
|
|
y = _GP(play).entered_at_y;
|
|
} else if (_GP(play).entered_at_x < 8) {
|
|
x = -8;
|
|
y = _GP(play).entered_at_y;
|
|
} else if (_GP(play).entered_at_y > (_GP(thisroom).Height - 8)) {
|
|
y = _GP(thisroom).Height + 8;
|
|
x = _GP(play).entered_at_x;
|
|
} else if (_GP(play).entered_at_y < _GP(thisroom).Edges.Top + 8) {
|
|
y = _GP(thisroom).Edges.Top + 1;
|
|
x = _GP(play).entered_at_x;
|
|
} else {
|
|
// not at one of the edges
|
|
// delay for a few seconds to let the player move
|
|
room = -_GP(play).follow_change_room_timer;
|
|
}
|
|
if (room >= 0) {
|
|
walk_character(aa, _GP(play).entered_at_x, _GP(play).entered_at_y, 1, true);
|
|
doing_nothing = 0;
|
|
}
|
|
}
|
|
} else if (room != _G(displayed_room)) {
|
|
// if the character is following another character and
|
|
// neither is in the current room, don't try to move
|
|
} else if ((abs(_GP(game).chars[following].x - x) > distaway + 30) ||
|
|
(abs(_GP(game).chars[following].y - y) > distaway + 30) ||
|
|
((followinfo & 0x00ff) == 0)) {
|
|
// in same room
|
|
int goxoffs = (Random(50) - 25);
|
|
// make sure he's not standing on top of the other man
|
|
if (goxoffs < 0) goxoffs -= distaway;
|
|
else goxoffs += distaway;
|
|
walk_character(aa, _GP(game).chars[following].x + goxoffs,
|
|
_GP(game).chars[following].y + (Random(50) - 25), 0, true);
|
|
doing_nothing = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CharacterInfo::update_character_idle(CharacterExtras *chex, int &doing_nothing) {
|
|
// no idle animation, so skip this bit
|
|
if (idleview < 1);
|
|
// currently playing idle anim
|
|
else if (idleleft < 0);
|
|
// not in the current room
|
|
else if (room != _G(displayed_room));
|
|
// they are moving or animating (or the view is locked), so
|
|
// reset idle timeout
|
|
else if ((doing_nothing == 0) || ((flags & CHF_FIXVIEW) != 0))
|
|
idleleft = idletime;
|
|
// count idle time
|
|
else if ((_G(loopcounter) % GetGameSpeed() == 0) || (chex->process_idle_this_time == 1)) {
|
|
idleleft--;
|
|
if (idleleft == -1) {
|
|
int useloop = loop;
|
|
debug_script_log("%s: Now idle (view %d)", scrname, idleview + 1);
|
|
Character_LockView(this, idleview + 1);
|
|
// SetCharView resets it to 0
|
|
idleleft = -2;
|
|
int maxLoops = _GP(views)[idleview].numLoops;
|
|
// if the char is set to "no diagonal loops", don't try
|
|
// to use diagonal idle loops either
|
|
if ((maxLoops > 4) && (useDiagonal(this)))
|
|
maxLoops = 4;
|
|
// If it's not a "swimming"-type idleanim, choose a random loop
|
|
// if there arent enough loops to do the current one.
|
|
if ((idletime > 0) && (useloop >= maxLoops)) {
|
|
do {
|
|
useloop = ::AGS::g_vm->getRandomNumber(maxLoops - 1);
|
|
// don't select a loop which is a continuation of a previous one
|
|
} while ((useloop > 0) && (_GP(views)[idleview].loops[useloop - 1].RunNextLoop()));
|
|
}
|
|
// Normal idle anim - just reset to loop 0 if not enough to
|
|
// use the current one
|
|
else if (useloop >= maxLoops)
|
|
useloop = 0;
|
|
|
|
animate_character(this, useloop, idle_anim_speed, (idletime == 0) ? 1 : 0 /* repeat */);
|
|
|
|
// don't set Animating while the idle anim plays (TODO: investigate why?)
|
|
animating = 0;
|
|
}
|
|
} // end do idle animation
|
|
}
|
|
|
|
} // namespace AGS3
|