/* 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 . * */ //============================================================================= // // AGS Character functions // //============================================================================= #include "ags/engine/ac/global_character.h" #include "ags/shared/ac/common.h" #include "ags/shared/ac/view.h" #include "ags/engine/ac/character.h" #include "ags/engine/ac/display.h" #include "ags/engine/ac/draw.h" #include "ags/engine/ac/event.h" #include "ags/shared/ac/game_setup_struct.h" #include "ags/engine/ac/game_state.h" #include "ags/engine/ac/global_overlay.h" #include "ags/engine/ac/global_translation.h" #include "ags/engine/ac/object.h" #include "ags/engine/ac/overlay.h" #include "ags/engine/ac/properties.h" #include "ags/engine/ac/screen_overlay.h" #include "ags/engine/ac/string.h" #include "ags/engine/ac/dynobj/cc_character.h" #include "ags/engine/debugging/debug_log.h" #include "ags/shared/game/room_struct.h" #include "ags/engine/main/game_run.h" #include "ags/engine/script/script.h" #include "ags/globals.h" namespace AGS3 { using namespace AGS::Shared; // defined in character unit void StopMoving(int chaa) { Character_StopMoving(&_GP(game).chars[chaa]); } void ReleaseCharacterView(int chat) { if (!is_valid_character(chat)) quit("!ReleaseCahracterView: invalid character supplied"); Character_UnlockView(&_GP(game).chars[chat]); } void MoveToWalkableArea(int charid) { if (!is_valid_character(charid)) quit("!MoveToWalkableArea: invalid character specified"); Character_PlaceOnWalkableArea(&_GP(game).chars[charid]); } void FaceLocation(int cha, int xx, int yy) { if (!is_valid_character(cha)) quit("!FaceLocation: Invalid character specified"); Character_FaceLocation(&_GP(game).chars[cha], xx, yy, BLOCKING); } void FaceCharacter(int cha, int toface) { if (!is_valid_character(cha)) quit("!FaceCharacter: Invalid character specified"); if (!is_valid_character(toface)) quit("!FaceCharacter: invalid character specified"); Character_FaceCharacter(&_GP(game).chars[cha], &_GP(game).chars[toface], BLOCKING); } void SetCharacterIdle(int who, int iview, int itime) { if (!is_valid_character(who)) quit("!SetCharacterIdle: Invalid character specified"); Character_SetIdleView(&_GP(game).chars[who], iview, itime); } int GetCharacterWidth(int ww) { CharacterInfo *char1 = &_GP(game).chars[ww]; if (_GP(charextra)[ww].width < 1) { if ((char1->view < 0) || (char1->loop >= _GP(views)[char1->view].numLoops) || (char1->frame >= _GP(views)[char1->view].loops[char1->loop].numFrames)) { debug_script_warn("GetCharacterWidth: Character %s has invalid frame: view %d, loop %d, frame %d", char1->scrname, char1->view + 1, char1->loop, char1->frame); return data_to_game_coord(4); } return _GP(game).SpriteInfos[_GP(views)[char1->view].loops[char1->loop].frames[char1->frame].pic].Width; } else return _GP(charextra)[ww].width; } int GetCharacterHeight(int charid) { CharacterInfo *char1 = &_GP(game).chars[charid]; if (_GP(charextra)[charid].height < 1) { if ((char1->view < 0) || (char1->loop >= _GP(views)[char1->view].numLoops) || (char1->frame >= _GP(views)[char1->view].loops[char1->loop].numFrames)) { debug_script_warn("GetCharacterHeight: Character %s has invalid frame: view %d, loop %d, frame %d", char1->scrname, char1->view + 1, char1->loop, char1->frame); return data_to_game_coord(2); } return _GP(game).SpriteInfos[_GP(views)[char1->view].loops[char1->loop].frames[char1->frame].pic].Height; } else return _GP(charextra)[charid].height; } void SetCharacterBaseline(int obn, int basel) { if (!is_valid_character(obn)) quit("!SetCharacterBaseline: invalid object number specified"); Character_SetBaseline(&_GP(game).chars[obn], basel); } // pass trans=0 for fully solid, trans=100 for fully transparent void SetCharacterTransparency(int obn, int trans) { if (!is_valid_character(obn)) quit("!SetCharTransparent: invalid character number specified"); Character_SetTransparency(&_GP(game).chars[obn], trans); } void AnimateCharacter4(int chh, int loopn, int sppd, int rept) { AnimateCharacter6(chh, loopn, sppd, rept, FORWARDS, IN_BACKGROUND); } void AnimateCharacter6(int chh, int loopn, int sppd, int rept, int direction, int blocking) { if (!is_valid_character(chh)) quit("AnimateCharacter: invalid character"); Character_Animate5(&_GP(game).chars[chh], loopn, sppd, rept, blocking, direction); } void SetPlayerCharacter(int newchar) { if (!is_valid_character(newchar)) quit("!SetPlayerCharacter: Invalid character specified"); Character_SetAsPlayer(&_GP(game).chars[newchar]); } void FollowCharacterEx(int who, int tofollow, int distaway, int eagerness) { if (!is_valid_character(who)) quit("!FollowCharacter: Invalid character specified"); CharacterInfo *chtofollow = nullptr; if (tofollow != -1) { if (!is_valid_character(tofollow)) quit("!FollowCharacterEx: invalid character to follow"); else chtofollow = &_GP(game).chars[tofollow]; } Character_FollowCharacter(&_GP(game).chars[who], chtofollow, distaway, eagerness); } void FollowCharacter(int who, int tofollow) { FollowCharacterEx(who, tofollow, 10, 97); } void SetCharacterIgnoreLight(int who, int yesorno) { if (!is_valid_character(who)) quit("!SetCharacterIgnoreLight: Invalid character specified"); Character_SetIgnoreLighting(&_GP(game).chars[who], yesorno); } void MoveCharacter(int cc, int xx, int yy) { walk_character(cc, xx, yy, 0, true); } void MoveCharacterDirect(int cc, int xx, int yy) { walk_character(cc, xx, yy, 1, true); } void MoveCharacterStraight(int cc, int xx, int yy) { if (!is_valid_character(cc)) quit("!MoveCharacterStraight: invalid character specified"); Character_WalkStraight(&_GP(game).chars[cc], xx, yy, IN_BACKGROUND); } // Append to character path void MoveCharacterPath(int chac, int tox, int toy) { if (!is_valid_character(chac)) quit("!MoveCharacterPath: invalid character specified"); Character_AddWaypoint(&_GP(game).chars[chac], tox, toy); } int GetPlayerCharacter() { return _GP(game).playercharacter; } void SetCharacterSpeedEx(int chaa, int xspeed, int yspeed) { if (!is_valid_character(chaa)) quit("!SetCharacterSpeedEx: invalid character"); Character_SetSpeed(&_GP(game).chars[chaa], xspeed, yspeed); } void SetCharacterSpeed(int chaa, int nspeed) { SetCharacterSpeedEx(chaa, nspeed, nspeed); } void SetTalkingColor(int chaa, int ncol) { if (!is_valid_character(chaa)) quit("!SetTalkingColor: invalid character"); Character_SetSpeechColor(&_GP(game).chars[chaa], ncol); } void SetCharacterSpeechView(int chaa, int vii) { if (!is_valid_character(chaa)) quit("!SetCharacterSpeechView: invalid character specified"); Character_SetSpeechView(&_GP(game).chars[chaa], vii); } void SetCharacterBlinkView(int chaa, int vii, int intrv) { if (!is_valid_character(chaa)) quit("!SetCharacterBlinkView: invalid character specified"); Character_SetBlinkView(&_GP(game).chars[chaa], vii); Character_SetBlinkInterval(&_GP(game).chars[chaa], intrv); } void SetCharacterView(int chaa, int vii) { if (!is_valid_character(chaa)) quit("!SetCharacterView: invalid character specified"); Character_LockView(&_GP(game).chars[chaa], vii); } void SetCharacterFrame(int chaa, int view, int loop, int frame) { Character_LockViewFrame(&_GP(game).chars[chaa], view, loop, frame); } // similar to SetCharView, but aligns the frame to make it line up void SetCharacterViewEx(int chaa, int vii, int loop, int align) { Character_LockViewAligned(&_GP(game).chars[chaa], vii, loop, ConvertLegacyScriptAlignment((LegacyScriptAlignment)align)); } void SetCharacterViewOffset(int chaa, int vii, int xoffs, int yoffs) { Character_LockViewOffset(&_GP(game).chars[chaa], vii, xoffs, yoffs); } void ChangeCharacterView(int chaa, int vii) { if (!is_valid_character(chaa)) quit("!ChangeCharacterView: invalid character specified"); Character_ChangeView(&_GP(game).chars[chaa], vii); } void SetCharacterClickable(int cha, int clik) { if (!is_valid_character(cha)) quit("!SetCharacterClickable: Invalid character specified"); // make the character clicklabe (reset "No interaction" bit) _GP(game).chars[cha].flags &= ~CHF_NOINTERACT; // if they don't want it clickable, set the relevant bit if (clik == 0) _GP(game).chars[cha].flags |= CHF_NOINTERACT; } void SetCharacterIgnoreWalkbehinds(int cha, int clik) { if (!is_valid_character(cha)) quit("!SetCharacterIgnoreWalkbehinds: Invalid character specified"); Character_SetIgnoreWalkbehinds(&_GP(game).chars[cha], clik); } void MoveCharacterToObject(int chaa, int obbj) { // invalid object, do nothing // this allows MoveCharacterToObject(EGO, GetObjectAt(...)); if (!is_valid_object(obbj)) return; walk_character(chaa, _G(objs)[obbj].x + 5, _G(objs)[obbj].y + 6, 0, true); GameLoopUntilNotMoving(&_GP(game).chars[chaa].walking); } void MoveCharacterToHotspot(int chaa, int hotsp) { if ((hotsp < 0) || (hotsp >= MAX_ROOM_HOTSPOTS)) quit("!MovecharacterToHotspot: invalid hotspot"); if (_GP(thisroom).Hotspots[hotsp].WalkTo.X < 1) return; walk_character(chaa, _GP(thisroom).Hotspots[hotsp].WalkTo.X, _GP(thisroom).Hotspots[hotsp].WalkTo.Y, 0, true); GameLoopUntilNotMoving(&_GP(game).chars[chaa].walking); } int MoveCharacterBlocking(int chaa, int xx, int yy, int direct) { if (!is_valid_character(chaa)) quit("!MoveCharacterBlocking: invalid character"); // check if they try to move the player when Hide Player Char is // ticked -- otherwise this will hang the game if (_GP(game).chars[chaa].on != 1) { debug_script_warn("MoveCharacterBlocking: character is turned off (is Hide Player Character selected?) and cannot be moved"); return 0; } if (direct) MoveCharacterDirect(chaa, xx, yy); else MoveCharacter(chaa, xx, yy); GameLoopUntilNotMoving(&_GP(game).chars[chaa].walking); return -1; // replicates legacy engine effect } int GetCharacterSpeechAnimationDelay(CharacterInfo *cha) { if ((_G(loaded_game_file_version) < kGameVersion_312) && (_GP(game).options[OPT_SPEECHTYPE] != 0)) { // legacy versions of AGS assigned a fixed delay to Sierra-style speech only return 5; } if (_GP(game).options[OPT_GLOBALTALKANIMSPD] != 0) return _GP(play).talkanim_speed; else return cha->speech_anim_speed; } void RunCharacterInteraction(int cc, int mood) { if (!is_valid_character(cc)) quit("!RunCharacterInteraction: invalid character"); // convert cursor mode to event index (in character event table) // TODO: probably move this conversion table elsewhere? should be a global info int evnt; switch (mood) { case MODE_LOOK: evnt = 0; break; case MODE_HAND: evnt = 1; break; case MODE_TALK: evnt = 2; break; case MODE_USE: evnt = 3; break; case MODE_PICKUP: evnt = 5; break; case MODE_CUSTOM1: evnt = 6; break; case MODE_CUSTOM2: evnt = 7; break; default: evnt = -1; break; } const int anyclick_evt = 4; // TODO: make global constant (character any-click evt) // For USE verb: remember active inventory if (mood == MODE_USE) { _GP(play).usedinv = _G(playerchar)->activeinv; } const auto obj_evt = ObjectEvent("character%d", cc, RuntimeScriptValue().SetScriptObject(&_GP(game).chars[cc], &_GP(ccDynamicCharacter)), mood); if (_G(loaded_game_file_version) > kGameVersion_272) { if ((evnt >= 0) && run_interaction_script(obj_evt, _GP(game).charScripts[cc].get(), evnt, anyclick_evt) < 0) return; // game state changed, don't do "any click" run_interaction_script(obj_evt, _GP(game).charScripts[cc].get(), anyclick_evt); // any click on char } else { if ((evnt >= 0) && run_interaction_event(obj_evt, _GP(game).intrChar[cc].get(), evnt, anyclick_evt, (mood == MODE_USE)) < 0) return; // game state changed, don't do "any click" run_interaction_event(obj_evt, _GP(game).intrChar[cc].get(), anyclick_evt); // any click on char } } int AreCharObjColliding(int charid, int objid) { if (!is_valid_character(charid)) quit("!AreCharObjColliding: invalid character"); if (!is_valid_object(objid)) quit("!AreCharObjColliding: invalid object number"); return Character_IsCollidingWithObject(&_GP(game).chars[charid], &_G(scrObj)[objid]); } int AreCharactersColliding(int cchar1, int cchar2) { if (!is_valid_character(cchar1)) quit("!AreCharactersColliding: invalid char1"); if (!is_valid_character(cchar2)) quit("!AreCharactersColliding: invalid char2"); return Character_IsCollidingWithChar(&_GP(game).chars[cchar1], &_GP(game).chars[cchar2]); } int GetCharacterProperty(int cha, const char *property) { if (!is_valid_character(cha)) quit("!GetCharacterProperty: invalid character"); return get_int_property(_GP(game).charProps[cha], _GP(play).charProps[cha], property); } void SetCharacterProperty(int who, int flag, int yesorno) { if (!is_valid_character(who)) quit("!SetCharacterProperty: Invalid character specified"); Character_SetOption(&_GP(game).chars[who], flag, yesorno); } void GetCharacterPropertyText(int item, const char *property, char *bufer) { get_text_property(_GP(game).charProps[item], _GP(play).charProps[item], property, bufer); } int GetCharIDAtScreen(int xx, int yy) { VpPoint vpt = _GP(play).ScreenToRoomDivDown(xx, yy); if (vpt.second < 0) return -1; return is_pos_on_character(vpt.first.X, vpt.first.Y); } void SetActiveInventory(int iit) { ScriptInvItem *tosend = nullptr; if ((iit > 0) && (iit < _GP(game).numinvitems)) tosend = &_G(scrInv)[iit]; else if (iit != -1) quitprintf("!SetActiveInventory: invalid inventory number %d", iit); Character_SetActiveInventory(_G(playerchar), tosend); } void update_invorder() { for (int cc = 0; cc < _GP(game).numcharacters; cc++) { _GP(charextra)[cc].invorder_count = 0; int ff, howmany; // Iterate through all inv items, adding them once (or multiple // times if requested) to the list. for (ff = 0; ff < _GP(game).numinvitems; ff++) { howmany = _GP(game).chars[cc].inv[ff]; if ((_GP(game).options[OPT_DUPLICATEINV] == 0) && (howmany > 1)) howmany = 1; for (int ts = 0; ts < howmany; ts++) { if (_GP(charextra)[cc].invorder_count >= MAX_INVORDER) quit("!Too many inventory items to display: 500 max"); _GP(charextra)[cc].invorder[_GP(charextra)[cc].invorder_count] = ff; _GP(charextra)[cc].invorder_count++; } } } // backwards compatibility _GP(play).inv_numorder = _GP(charextra)[_GP(game).playercharacter].invorder_count; GUI::MarkInventoryForUpdate(_GP(game).playercharacter, true); } void add_inventory(int inum) { if ((inum < 0) || (inum >= MAX_INV)) quit("!AddInventory: invalid inventory number"); Character_AddInventory(_G(playerchar), &_G(scrInv)[inum], SCR_NO_VALUE); _GP(play).inv_numorder = _GP(charextra)[_GP(game).playercharacter].invorder_count; } void lose_inventory(int inum) { if ((inum < 0) || (inum >= MAX_INV)) quit("!LoseInventory: invalid inventory number"); Character_LoseInventory(_G(playerchar), &_G(scrInv)[inum]); _GP(play).inv_numorder = _GP(charextra)[_GP(game).playercharacter].invorder_count; } void AddInventoryToCharacter(int charid, int inum) { if (!is_valid_character(charid)) quit("!AddInventoryToCharacter: invalid character specified"); if ((inum < 1) || (inum >= _GP(game).numinvitems)) quit("!AddInventory: invalid inv item specified"); Character_AddInventory(&_GP(game).chars[charid], &_G(scrInv)[inum], SCR_NO_VALUE); } void LoseInventoryFromCharacter(int charid, int inum) { if (!is_valid_character(charid)) quit("!LoseInventoryFromCharacter: invalid character specified"); if ((inum < 1) || (inum >= _GP(game).numinvitems)) quit("!AddInventory: invalid inv item specified"); Character_LoseInventory(&_GP(game).chars[charid], &_G(scrInv)[inum]); } void DisplayThought(int chid, const char *text) { if ((chid < 0) || (chid >= _GP(game).numcharacters)) quit("!DisplayThought: invalid character specified"); _DisplayThoughtCore(chid, text); } void __sc_displayspeech(int chid, const char *text) { if ((chid < 0) || (chid >= _GP(game).numcharacters)) quit("!DisplaySpeech: invalid character specified"); _DisplaySpeechCore(chid, text); } // **** THIS IS UNDOCUMENTED BECAUSE IT DOESN'T WORK PROPERLY // **** AT 640x400 AND DOESN'T USE THE RIGHT SPEECH STYLE void DisplaySpeechAt(int xx, int yy, int wii, int aschar, const char *spch) { data_to_game_coords(&xx, &yy); wii = data_to_game_coord(wii); _displayspeech(get_translation(spch), aschar, xx, yy, wii, 0); } int DisplaySpeechBackground(int charid, const char *speel) { // remove any previous background speech for this character // TODO: have a map character -> bg speech over? const auto &overs = get_overlays(); for (size_t i = 0; i < overs.size(); ++i) { if (overs[i].bgSpeechForChar == charid) { remove_screen_overlay(i); break; } } int ovrl = CreateTextOverlay(OVR_AUTOPLACE, charid, _GP(play).GetUIViewport().GetWidth() / 2, FONT_SPEECH, -_GP(game).chars[charid].talkcolor, get_translation(speel), DISPLAYTEXT_NORMALOVERLAY); auto *over = get_overlay(ovrl); over->bgSpeechForChar = charid; over->timeout = GetTextDisplayTime(speel, 1); return ovrl; } } // namespace AGS3