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
|