/* 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 "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 &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 &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