#include "towners.h" #include "cursor.h" #include "inv.h" #include "minitext.h" #include "stores.h" #include "utils/language.h" namespace devilution { namespace { std::unique_ptr CowCels; int CowMsg; int CowClicks; /** * Maps from direction to coordinate delta, which is used when * placing cows in Tristram. A single cow may require space of up * to three tiles when being placed on the map. */ Point CowOffsets[8] = { { -1, -1 }, { 0, -1 }, { -1, -1 }, { -1, 0 }, { -1, -1 }, { 0, -1 }, { -1, -1 }, { -1, 0 } }; /** Specifies the active sound effect ID for interacting with cows. */ _sfx_id CowPlaying = SFX_NONE; struct TownerInit { _talker_id type; Point position; direction dir; void (*init)(TownerStruct &towner, const TownerInit &initData); void (*talk)(PlayerStruct &player, TownerStruct &towner); }; void NewTownerAnim(TownerStruct &towner, byte *pAnim, uint8_t numFrames, int delay) { towner._tAnimData = pAnim; towner._tAnimLen = numFrames; towner._tAnimFrame = 1; towner._tAnimCnt = 0; towner._tAnimDelay = delay; } void InitTownerInfo(int i, const TownerInit &initData) { auto &towner = towners[i]; towner._ttype = initData.type; towner.position = initData.position; towner.talk = initData.talk; towner._tSeed = AdvanceRndSeed(); dMonster[towner.position.x][towner.position.y] = i + 1; initData.init(towner, initData); } void InitQstSnds(TownerStruct &towner, _talker_id type) { for (int i = 0; i < MAXQUESTS; i++) { towner.qsts[i]._qsttype = quests[i]._qtype; towner.qsts[i]._qstmsg = Qtalklist[type][i]; towner.qsts[i]._qstmsgact = Qtalklist[type][i] != TEXT_NONE; } } void LoadTownerAnimations(TownerStruct &towner, const char *path, int frames, direction dir, int delay) { towner._tNData = LoadFileInMem(path); for (auto &animation : towner._tNAnim) { animation = towner._tNData.get(); } NewTownerAnim(towner, towner._tNAnim[dir], frames, delay); } /** * @brief Load Griswold into the game */ void InitSmith(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); towner._tAnimWidth = 96; static const uint8_t AnimOrder[] = { // clang-format off 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4 // clang-format on }; towner.animOrder = AnimOrder; towner.animOrderSize = sizeof(AnimOrder); LoadTownerAnimations(towner, "Towners\\Smith\\SmithN.CEL", 16, initData.dir, 3); towner._tName = _("Griswold the Blacksmith"); } void InitBarOwner(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); towner._tAnimWidth = 96; static const uint8_t AnimOrder[] = { // clang-format off 1, 2, 3, 3, 2, 1, 16, 15, 14, 14, 15, 16, 1, 2, 3, 3, 2, 1, 16, 15, 14, 14, 15, 16, 1, 2, 3, 3, 2, 1, 16, 15, 14, 14, 15, 16, 1, 2, 3, 3, 2, 1, 16, 15, 14, 14, 15, 16, 1, 2, 3, 3, 2, 1, 16, 15, 14, 14, 15, 16, 1, 2, 3, 3, 2, 1, 16, 15, 14, 14, 15, 16, 1, 2, 3, 3, 2, 1, 16, 15, 14, 14, 15, 16, 1, 2, 3, 2, 1, 16, 15, 14, 14, 15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 // clang-format on }; towner.animOrder = AnimOrder; towner.animOrderSize = sizeof(AnimOrder); LoadTownerAnimations(towner, "Towners\\TwnF\\TwnFN.CEL", 16, initData.dir, 3); towner._tName = _("Ogden the Tavern owner"); } void InitTownDead(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); towner._tAnimWidth = 96; towner.animOrder = nullptr; towner.animOrderSize = 0; LoadTownerAnimations(towner, "Towners\\Butch\\Deadguy.CEL", 8, initData.dir, 6); towner._tName = _("Wounded Townsman"); } void InitWitch(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); towner._tAnimWidth = 96; static const uint8_t AnimOrder[] = { // clang-format off 4, 4, 4, 5, 6, 6, 6, 5, 4, 15, 14, 13, 13, 13, 14, 15, 4, 5, 6, 6, 6, 5, 4, 4, 4, 5, 6, 6, 6, 5, 4, 15, 14, 13, 13, 13, 14, 15, 4, 5, 6, 6, 6, 5, 4, 4, 4, 5, 6, 6, 6, 5, 4, 15, 14, 13, 13, 13, 14, 15, 4, 5, 6, 6, 6, 5, 4, 3, 2, 1, 19, 18, 19, 1, 2, 1, 19, 18, 19, 1, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 14, 13, 13, 13, 13, 14, 15, 15, 15, 14, 13, 12, 12, 12, 11, 10, 10, 10, 9, 8, 9, 10, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1, 2, 1, 19, 18, 19, 1, 2, 1, 2, 3 // clang-format on }; towner.animOrder = AnimOrder; towner.animOrderSize = sizeof(AnimOrder); LoadTownerAnimations(towner, "Towners\\TownWmn1\\Witch.CEL", 19, initData.dir, 6); towner._tName = _("Adria the Witch"); } void InitBarmaid(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); towner._tAnimWidth = 96; towner.animOrder = nullptr; towner.animOrderSize = 0; LoadTownerAnimations(towner, "Towners\\TownWmn1\\WmnN.CEL", 18, initData.dir, 6); towner._tName = _("Gillian the Barmaid"); } void InitBoy(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); towner._tAnimWidth = 96; towner.animOrder = nullptr; towner.animOrderSize = 0; LoadTownerAnimations(towner, "Towners\\TownBoy\\PegKid1.CEL", 20, initData.dir, 6); towner._tName = _("Wirt the Peg-legged boy"); } void InitHealer(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); towner._tAnimWidth = 96; static const uint8_t AnimOrder[] = { // clang-format off 1, 2, 3, 3, 2, 1, 20, 19, 19, 20, 1, 2, 3, 3, 2, 1, 20, 19, 19, 20, 1, 2, 3, 3, 2, 1, 20, 19, 19, 20, 1, 2, 3, 3, 2, 1, 20, 19, 19, 20, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 // clang-format on }; towner.animOrder = AnimOrder; towner.animOrderSize = sizeof(AnimOrder); LoadTownerAnimations(towner, "Towners\\Healer\\Healer.CEL", 20, initData.dir, 6); towner._tName = _("Pepin the Healer"); } void InitTeller(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); towner._tAnimWidth = 96; static const uint8_t AnimOrder[] = { // clang-format off 1, 1, 25, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 25, 25, 1, 1, 1, 25, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 // clang-format on }; towner.animOrder = AnimOrder; towner.animOrderSize = sizeof(AnimOrder); LoadTownerAnimations(towner, "Towners\\Strytell\\Strytell.CEL", 25, initData.dir, 3); towner._tName = _("Cain the Elder"); } void InitDrunk(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); towner._tAnimWidth = 96; static const uint8_t AnimOrder[] = { // clang-format off 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11, 11, 11, 12, 13, 14, 15, 16, 17, 18, 18, 1, 1, 1, 18, 17, 16, 15, 14, 13, 12, 11, 10, 11, 12, 13, 14, 15, 16, 17, 18, 1, 2, 3, 4, 5, 5, 5, 4, 3, 2 // clang-format off }; towner.animOrder = AnimOrder; towner.animOrderSize = sizeof(AnimOrder); LoadTownerAnimations(towner, "Towners\\Drunk\\TwnDrunk.CEL", 18, initData.dir, 3); towner._tName = _("Farnham the Drunk"); } void InitCows(TownerStruct &towner, const TownerInit &initData) { towner._tAnimWidth = 128; towner.animOrder = nullptr; towner.animOrderSize = 0; for (int i = 0; i < 8; i++) { towner._tNAnim[i] = CelGetFrameStart(CowCels.get(), i); } NewTownerAnim(towner, towner._tNAnim[initData.dir], 12, 3); towner._tAnimFrame = GenerateRnd(11) + 1; towner._tName = _("Cow"); const Point position = initData.position; const Point offset = position + CowOffsets[initData.dir]; int index = -dMonster[position.x][position.y]; if (dMonster[position.x][offset.y] == 0) dMonster[position.x][offset.y] = index; if (dMonster[offset.x][position.y] == 0) dMonster[offset.x][position.y] = index; if (dMonster[offset.x][offset.y] == 0) dMonster[offset.x][offset.y] = index; } void InitFarmer(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); towner._tAnimWidth = 96; towner.animOrder = nullptr; towner.animOrderSize = 0; LoadTownerAnimations(towner, "Towners\\Farmer\\Farmrn2.CEL", 15, initData.dir, 3); towner._tName = _("Lester the farmer"); } void InitCowFarmer(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); const char *celPath = "Towners\\Farmer\\cfrmrn2.CEL"; if (quests[Q_JERSEY]._qactive == QUEST_DONE) { celPath = "Towners\\Farmer\\mfrmrn2.CEL"; } towner._tAnimWidth = 96; towner.animOrder = nullptr; towner.animOrderSize = 0; LoadTownerAnimations(towner, celPath, 15, initData.dir, 3); towner._tName = _("Complete Nut"); } void InitGirl(TownerStruct &towner, const TownerInit &initData) { InitQstSnds(towner, initData.type); const char *celPath = "Towners\\Girl\\Girlw1.CEL"; if (quests[Q_GIRL]._qactive == QUEST_DONE) { celPath = "Towners\\Girl\\Girls1.CEL"; } towner._tAnimWidth = 96; towner.animOrder = nullptr; towner.animOrderSize = 0; LoadTownerAnimations(towner, celPath, 20, initData.dir, 6); towner._tName = "Celia"; } void TownDead(TownerStruct &towner) { if (qtextflag) { if (quests[Q_BUTCHER]._qvar1 == 1) towner._tAnimCnt = 0; // Freeze while speaking return; } if ((quests[Q_BUTCHER]._qactive == QUEST_DONE || quests[Q_BUTCHER]._qvar1 == 1) && towner._tAnimLen != 1) { towner._tAnimLen = 1; towner._tName = _("Slain Townsman"); } } void TownerTalk(_speech_id message) { CowClicks = 0; CowMsg = 0; InitQTextMsg(message); } void TalkToBarOwner(PlayerStruct &player, TownerStruct &barOwner) { if (!player._pLvlVisited[0]) { InitQTextMsg(TEXT_INTRO); return; } if (quests[Q_SKELKING]._qactive != QUEST_NOTAVAIL) { if (player._pLvlVisited[2] || player._pLvlVisited[4]) { if (quests[Q_SKELKING]._qvar2 == 0) { quests[Q_SKELKING]._qvar2 = 1; quests[Q_SKELKING]._qlog = true; if (quests[Q_SKELKING]._qactive == QUEST_INIT) { quests[Q_SKELKING]._qactive = QUEST_ACTIVE; quests[Q_SKELKING]._qvar1 = 1; } InitQTextMsg(TEXT_KING2); NetSendCmdQuest(true, Q_SKELKING); return; } if (quests[Q_SKELKING]._qactive == QUEST_DONE && quests[Q_SKELKING]._qvar2 == 1) { quests[Q_SKELKING]._qvar2 = 2; quests[Q_SKELKING]._qvar1 = 2; InitQTextMsg(TEXT_KING4); NetSendCmdQuest(true, Q_SKELKING); return; } } } if (quests[Q_LTBANNER]._qactive != QUEST_NOTAVAIL) { if (player._pLvlVisited[3] && quests[Q_LTBANNER]._qactive != QUEST_DONE) { if (quests[Q_LTBANNER]._qvar2 == 0) { quests[Q_LTBANNER]._qvar2 = 1; if (quests[Q_LTBANNER]._qactive == QUEST_INIT) { quests[Q_LTBANNER]._qvar1 = 1; quests[Q_LTBANNER]._qactive = QUEST_ACTIVE; } quests[Q_LTBANNER]._qlog = true; InitQTextMsg(TEXT_BANNER2); return; } int i; if (quests[Q_LTBANNER]._qvar2 == 1 && player.HasItem(IDI_BANNER, &i)) { quests[Q_LTBANNER]._qactive = QUEST_DONE; quests[Q_LTBANNER]._qvar1 = 3; player.RemoveInvItem(i); SpawnUnique(UITEM_HARCREST, barOwner.position.x, barOwner.position.y + 1); InitQTextMsg(TEXT_BANNER3); return; } } } TownerTalk(TEXT_OGDEN1); StartStore(STORE_TAVERN); } void TalkToDeadguy(PlayerStruct &player, TownerStruct & /*deadguy*/) { if (quests[Q_BUTCHER]._qactive == QUEST_DONE) return; if (quests[Q_BUTCHER]._qvar1 == 1) { player.PlaySpecificSpeach(8); return; } quests[Q_BUTCHER]._qactive = QUEST_ACTIVE; quests[Q_BUTCHER]._qlog = true; quests[Q_BUTCHER]._qmsg = TEXT_BUTCH9; quests[Q_BUTCHER]._qvar1 = 1; InitQTextMsg(TEXT_BUTCH9); NetSendCmdQuest(true, Q_BUTCHER); } void TalkToBlackSmith(PlayerStruct &player, TownerStruct &blackSmith) { if (quests[Q_ROCK]._qactive != QUEST_NOTAVAIL) { if (player._pLvlVisited[4] && quests[Q_ROCK]._qactive != QUEST_DONE) { if (quests[Q_ROCK]._qvar2 == 0) { quests[Q_ROCK]._qvar2 = 1; quests[Q_ROCK]._qlog = true; if (quests[Q_ROCK]._qactive == QUEST_INIT) { quests[Q_ROCK]._qactive = QUEST_ACTIVE; } InitQTextMsg(TEXT_INFRA5); return; } int i; if (quests[Q_ROCK]._qvar2 == 1 && player.HasItem(IDI_ROCK, &i)) { quests[Q_ROCK]._qactive = QUEST_DONE; player.RemoveInvItem(i); SpawnUnique(UITEM_INFRARING, blackSmith.position.x, blackSmith.position.y + 1); InitQTextMsg(TEXT_INFRA7); return; } } } if (quests[Q_ANVIL]._qactive != QUEST_NOTAVAIL) { if (player._pLvlVisited[9] && quests[Q_ANVIL]._qactive != QUEST_DONE) { if (quests[Q_ANVIL]._qvar2 == 0 && quests[Q_ROCK]._qactive != QUEST_INIT) { quests[Q_ANVIL]._qvar2 = 1; quests[Q_ANVIL]._qlog = true; if (quests[Q_ANVIL]._qactive == QUEST_INIT) { quests[Q_ANVIL]._qactive = QUEST_ACTIVE; } InitQTextMsg(TEXT_ANVIL5); return; } int i; if (quests[Q_ANVIL]._qvar2 == 1 && player.HasItem(IDI_ANVIL, &i)) { quests[Q_ANVIL]._qactive = QUEST_DONE; player.RemoveInvItem(i); SpawnUnique(UITEM_GRISWOLD, blackSmith.position.x, blackSmith.position.y + 1); InitQTextMsg(TEXT_ANVIL7); return; } } } TownerTalk(TEXT_GRISWOLD1); StartStore(STORE_SMITH); } void TalkToWitch(PlayerStruct &player, TownerStruct & /*witch*/) { if (quests[Q_MUSHROOM]._qactive != QUEST_NOTAVAIL) { int i; if (quests[Q_MUSHROOM]._qactive == QUEST_INIT && player.HasItem(IDI_FUNGALTM, &i)) { player.RemoveInvItem(i); quests[Q_MUSHROOM]._qactive = QUEST_ACTIVE; quests[Q_MUSHROOM]._qlog = true; quests[Q_MUSHROOM]._qvar1 = QS_TOMEGIVEN; InitQTextMsg(TEXT_MUSH8); return; } if (quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE) { if (quests[Q_MUSHROOM]._qvar1 >= QS_TOMEGIVEN && quests[Q_MUSHROOM]._qvar1 < QS_MUSHGIVEN) { int i; if (player.HasItem(IDI_MUSHROOM, &i)) { player.RemoveInvItem(i); quests[Q_MUSHROOM]._qvar1 = QS_MUSHGIVEN; Qtalklist[TOWN_HEALER][Q_MUSHROOM] = TEXT_MUSH3; Qtalklist[TOWN_WITCH][Q_MUSHROOM] = TEXT_NONE; quests[Q_MUSHROOM]._qmsg = TEXT_MUSH10; InitQTextMsg(TEXT_MUSH10); return; } if (quests[Q_MUSHROOM]._qmsg != TEXT_MUSH9) { quests[Q_MUSHROOM]._qmsg = TEXT_MUSH9; InitQTextMsg(TEXT_MUSH9); return; } } if (quests[Q_MUSHROOM]._qvar1 >= QS_MUSHGIVEN) { if (player.HasItem(IDI_BRAIN)) { quests[Q_MUSHROOM]._qmsg = TEXT_MUSH11; InitQTextMsg(TEXT_MUSH11); return; } if (player.HasItem(IDI_SPECELIX)) { InitQTextMsg(TEXT_MUSH12); quests[Q_MUSHROOM]._qactive = QUEST_DONE; AllItemsList[IDI_SPECELIX].iUsable = true; // TODO modefy item instead of AllItemsList return; } } } } TownerTalk(TEXT_ADRIA1); StartStore(STORE_WITCH); } void TalkToBarmaid(PlayerStruct &player, TownerStruct & /*barmaid*/) { if (!player._pLvlVisited[21] && player.HasItem(IDI_MAPOFDOOM)) { quests[Q_GRAVE]._qactive = QUEST_ACTIVE; quests[Q_GRAVE]._qlog = true; quests[Q_GRAVE]._qmsg = TEXT_GRAVE8; InitQTextMsg(TEXT_GRAVE8); return; } TownerTalk(TEXT_GILLIAN1); StartStore(STORE_BARMAID); } void TalkToDrunk(PlayerStruct & /*player*/, TownerStruct & /*drunk*/) { TownerTalk(TEXT_FARNHAM1); StartStore(STORE_DRUNK); } void TalkToHealer(PlayerStruct &player, TownerStruct &healer) { if (quests[Q_PWATER]._qactive != QUEST_NOTAVAIL) { if ((player._pLvlVisited[1] || player._pLvlVisited[5]) && quests[Q_PWATER]._qactive == QUEST_INIT) { quests[Q_PWATER]._qactive = QUEST_ACTIVE; quests[Q_PWATER]._qlog = true; quests[Q_PWATER]._qmsg = TEXT_POISON3; InitQTextMsg(TEXT_POISON3); return; } if (quests[Q_PWATER]._qactive == QUEST_DONE && quests[Q_PWATER]._qvar1 != 2) { quests[Q_PWATER]._qvar1 = 2; InitQTextMsg(TEXT_POISON5); SpawnUnique(UITEM_TRING, healer.position.x, healer.position.y + 1); return; } } int i; if (quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE) { if (quests[Q_MUSHROOM]._qvar1 >= QS_MUSHGIVEN && quests[Q_MUSHROOM]._qvar1 < QS_BRAINGIVEN && player.HasItem(IDI_BRAIN, &i)) { player.RemoveInvItem(i); SpawnQuestItem(IDI_SPECELIX, healer.position + Point{0, 1}, 0, 0); InitQTextMsg(TEXT_MUSH4); quests[Q_MUSHROOM]._qvar1 = QS_BRAINGIVEN; Qtalklist[TOWN_HEALER][Q_MUSHROOM] = TEXT_NONE; return; } } TownerTalk(TEXT_PEPIN1); StartStore(STORE_HEALER); } void TalkToBoy(PlayerStruct & /*player*/, TownerStruct & /*boy*/) { TownerTalk(TEXT_WIRT1); StartStore(STORE_BOY); } void TalkToStoryteller(PlayerStruct &player, TownerStruct & /*storyteller*/) { if (!gbIsMultiplayer) { int i; if (quests[Q_BETRAYER]._qactive == QUEST_INIT && player.HasItem(IDI_LAZSTAFF, &i)) { InitQTextMsg(TEXT_VILE1); quests[Q_BETRAYER]._qlog = true; quests[Q_BETRAYER]._qactive = QUEST_ACTIVE; quests[Q_BETRAYER]._qvar1 = 2; player.RemoveInvItem(i); return; } } else { if (quests[Q_BETRAYER]._qactive == QUEST_ACTIVE && !quests[Q_BETRAYER]._qlog) { InitQTextMsg(TEXT_VILE1); quests[Q_BETRAYER]._qlog = true; NetSendCmdQuest(true, Q_BETRAYER); return; } } if (quests[Q_BETRAYER]._qactive == QUEST_DONE && quests[Q_BETRAYER]._qvar1 == 7) { quests[Q_BETRAYER]._qvar1 = 8; InitQTextMsg(TEXT_VILE3); quests[Q_DIABLO]._qlog = true; if (gbIsMultiplayer) { NetSendCmdQuest(true, Q_BETRAYER); NetSendCmdQuest(true, Q_DIABLO); } return; } TownerTalk(TEXT_STORY1); StartStore(STORE_STORY); } void TalkToCow(PlayerStruct &player, TownerStruct &cow) { if (CowPlaying != SFX_NONE && effect_is_playing(CowPlaying)) return; CowClicks++; CowPlaying = TSFX_COW1; if (CowClicks == 4) { if (gbIsSpawn) CowClicks = 0; CowPlaying = TSFX_COW2; } else if (CowClicks >= 8 && !gbIsSpawn) { CowClicks = 4; static const int snSfx[3] = { 52, 49, 50 }; player.PlaySpecificSpeach(snSfx[CowMsg]); CowMsg++; if (CowMsg >= 3) CowMsg = 0; } PlaySfxLoc(CowPlaying, cow.position.x, cow.position.y); } void TalkToFarmer(PlayerStruct &player, TownerStruct &farmer) { switch (quests[Q_FARMER]._qactive) { case QUEST_NOTAVAIL: case QUEST_INIT: if (player.HasItem(IDI_RUNEBOMB)) { InitQTextMsg(TEXT_FARMER2); quests[Q_FARMER]._qactive = QUEST_ACTIVE; quests[Q_FARMER]._qvar1 = 1; quests[Q_FARMER]._qmsg = TEXT_FARMER1; quests[Q_FARMER]._qlog = true; if (gbIsMultiplayer) NetSendCmdQuest(true, Q_FARMER); break; } if (!player._pLvlVisited[9] && player._pLevel < 15) { _speech_id qt = TEXT_FARMER8; if (player._pLvlVisited[2]) qt = TEXT_FARMER5; if (player._pLvlVisited[5]) qt = TEXT_FARMER7; if (player._pLvlVisited[7]) qt = TEXT_FARMER9; InitQTextMsg(qt); break; } InitQTextMsg(TEXT_FARMER1); quests[Q_FARMER]._qactive = QUEST_ACTIVE; quests[Q_FARMER]._qvar1 = 1; quests[Q_FARMER]._qlog = true; quests[Q_FARMER]._qmsg = TEXT_FARMER1; SpawnRuneBomb(farmer.position + Point{1, 0}); if (gbIsMultiplayer) NetSendCmdQuest(true, Q_FARMER); break; case QUEST_ACTIVE: InitQTextMsg(player.HasItem(IDI_RUNEBOMB) ? TEXT_FARMER2 : TEXT_FARMER3); break; case QUEST_DONE: InitQTextMsg(TEXT_FARMER4); SpawnRewardItem(IDI_AURIC, farmer.position + Point{1, 0}); quests[Q_FARMER]._qactive = QUEST_HIVE_DONE; quests[Q_FARMER]._qlog = false; if (gbIsMultiplayer) NetSendCmdQuest(true, Q_FARMER); break; default: InitQTextMsg(TEXT_FARMER4); break; } } void TalkToCowFarmer(PlayerStruct &player, TownerStruct &cowFarmer) { int i; if (player.HasItem(IDI_GREYSUIT, &i)) { InitQTextMsg(TEXT_JERSEY7); player.RemoveInvItem(i); } if (player.HasItem(IDI_BROWNSUIT, &i)) { SpawnUnique(UITEM_BOVINE, cowFarmer.position.x + 1, cowFarmer.position.y); player.RemoveInvItem(i); InitQTextMsg(TEXT_JERSEY8); quests[Q_JERSEY]._qactive = QUEST_DONE; return; } if (player.HasItem(IDI_RUNEBOMB)) { InitQTextMsg(TEXT_JERSEY5); quests[Q_JERSEY]._qactive = QUEST_ACTIVE; quests[Q_JERSEY]._qvar1 = 1; quests[Q_JERSEY]._qmsg = TEXT_JERSEY4; quests[Q_JERSEY]._qlog = true; return; } switch (quests[Q_JERSEY]._qactive) { case QUEST_NOTAVAIL: case QUEST_INIT: InitQTextMsg(TEXT_JERSEY1); quests[Q_JERSEY]._qactive = QUEST_HIVE_TEASE1; if (gbIsMultiplayer) NetSendCmdQuest(true, Q_JERSEY); break; case QUEST_ACTIVE: InitQTextMsg(TEXT_JERSEY5); break; case QUEST_DONE: InitQTextMsg(TEXT_JERSEY1); break; case QUEST_HIVE_TEASE1: InitQTextMsg(TEXT_JERSEY2); quests[Q_JERSEY]._qactive = QUEST_HIVE_TEASE2; if (gbIsMultiplayer) NetSendCmdQuest(true, Q_JERSEY); break; case QUEST_HIVE_TEASE2: InitQTextMsg(TEXT_JERSEY3); quests[Q_JERSEY]._qactive = QUEST_HIVE_ACTIVE; if (gbIsMultiplayer) NetSendCmdQuest(true, Q_JERSEY); break; case QUEST_HIVE_ACTIVE: if (!player._pLvlVisited[9] && player._pLevel < 15) { _speech_id qt = TEXT_JERSEY12; switch (GenerateRnd(4)) { case 0: qt = TEXT_JERSEY9; break; case 1: qt = TEXT_JERSEY10; break; case 2: qt = TEXT_JERSEY11; break; } InitQTextMsg(qt); break; } InitQTextMsg(TEXT_JERSEY4); quests[Q_JERSEY]._qactive = QUEST_ACTIVE; quests[Q_JERSEY]._qvar1 = 1; quests[Q_JERSEY]._qmsg = TEXT_JERSEY4; quests[Q_JERSEY]._qlog = true; SpawnRuneBomb(cowFarmer.position + Point{1, 0}); if (gbIsMultiplayer) NetSendCmdQuest(true, Q_JERSEY); break; default: InitQTextMsg(TEXT_JERSEY5); break; } } void TalkToGirl(PlayerStruct &player, TownerStruct &girl) { int i; if (player.HasItem(IDI_THEODORE, &i) && quests[Q_GIRL]._qactive != QUEST_DONE) { InitQTextMsg(TEXT_GIRL4); player.RemoveInvItem(i); CreateAmulet(girl.position, 13, false, true); quests[Q_GIRL]._qlog = false; quests[Q_GIRL]._qactive = QUEST_DONE; if (gbIsMultiplayer) NetSendCmdQuest(true, Q_GIRL); } switch (quests[Q_GIRL]._qactive) { case QUEST_NOTAVAIL: case QUEST_INIT: InitQTextMsg(TEXT_GIRL2); quests[Q_GIRL]._qactive = QUEST_ACTIVE; quests[Q_GIRL]._qvar1 = 1; quests[Q_GIRL]._qlog = true; quests[Q_GIRL]._qmsg = TEXT_GIRL2; if (gbIsMultiplayer) NetSendCmdQuest(true, Q_GIRL); return; case QUEST_ACTIVE: InitQTextMsg(TEXT_GIRL3); return; default: PlaySFX(alltext[TEXT_GIRL1].sfxnr); return; } } const TownerInit TownerInitList[] = { // clang-format off // type position dir init talk { TOWN_SMITH, { 62, 63 }, DIR_SW, InitSmith, TalkToBlackSmith }, { TOWN_HEALER, { 55, 79 }, DIR_SE, InitHealer, TalkToHealer }, { TOWN_DEADGUY, { 24, 32 }, DIR_N, InitTownDead, TalkToDeadguy }, { TOWN_TAVERN, { 55, 62 }, DIR_SW, InitBarOwner, TalkToBarOwner }, { TOWN_STORY, { 62, 71 }, DIR_S, InitTeller, TalkToStoryteller }, { TOWN_DRUNK, { 71, 84 }, DIR_S, InitDrunk, TalkToDrunk }, { TOWN_WITCH, { 80, 20 }, DIR_S, InitWitch, TalkToWitch }, { TOWN_BMAID, { 43, 66 }, DIR_S, InitBarmaid, TalkToBarmaid }, { TOWN_PEGBOY, { 11, 53 }, DIR_S, InitBoy, TalkToBoy }, { TOWN_COW, { 58, 16 }, DIR_SW, InitCows, TalkToCow }, { TOWN_COW, { 56, 14 }, DIR_NW, InitCows, TalkToCow }, { TOWN_COW, { 59, 20 }, DIR_N, InitCows, TalkToCow }, { TOWN_COWFARM, { 61, 22 }, DIR_SW, InitCowFarmer, TalkToCowFarmer }, { TOWN_FARMER, { 62, 16 }, DIR_S, InitFarmer, TalkToFarmer }, { TOWN_GIRL, { 77, 43 }, DIR_S, InitGirl, TalkToGirl }, // clang-format on }; } // namespace TownerStruct towners[NUM_TOWNERS]; /** Contains the data related to quest gossip for each towner ID. */ _speech_id Qtalklist[NUM_TOWNER_TYPES][MAXQUESTS] = { // clang-format off // Q_ROCK, Q_MUSHROOM, Q_GARBUD, Q_ZHAR, Q_VEIL, Q_DIABLO, Q_BUTCHER, Q_LTBANNER, Q_BLIND, Q_BLOOD, Q_ANVIL, Q_WARLORD, Q_SKELKING, Q_PWATER, Q_SCHAMB, Q_BETRAYER, Q_GRAVE, Q_FARMER, Q_GIRL, Q_TRADER, Q_DEFILER, Q_NAKRUL, Q_CORNSTN, Q_JERSEY /*TOWN_SMITH*/ { TEXT_INFRA6, TEXT_MUSH6, TEXT_NONE, TEXT_NONE, TEXT_VEIL5, TEXT_NONE, TEXT_BUTCH5, TEXT_BANNER6, TEXT_BLIND5, TEXT_BLOOD5, TEXT_ANVIL6, TEXT_WARLRD5, TEXT_KING7, TEXT_POISON7, TEXT_BONE5, TEXT_VILE9, TEXT_GRAVE2, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_HEALER*/ { TEXT_INFRA3, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_VEIL3, TEXT_NONE, TEXT_BUTCH3, TEXT_BANNER4, TEXT_BLIND3, TEXT_BLOOD3, TEXT_ANVIL3, TEXT_WARLRD3, TEXT_KING5, TEXT_POISON4, TEXT_BONE3, TEXT_VILE7, TEXT_GRAVE3, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_DEADGUY*/ { TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_TAVERN*/ { TEXT_INFRA2, TEXT_MUSH2, TEXT_NONE, TEXT_NONE, TEXT_VEIL2, TEXT_NONE, TEXT_BUTCH2, TEXT_NONE, TEXT_BLIND2, TEXT_BLOOD2, TEXT_ANVIL2, TEXT_WARLRD2, TEXT_KING3, TEXT_POISON2, TEXT_BONE2, TEXT_VILE4, TEXT_GRAVE5, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_STORY*/ { TEXT_INFRA1, TEXT_MUSH1, TEXT_NONE, TEXT_NONE, TEXT_VEIL1, TEXT_VILE3, TEXT_BUTCH1, TEXT_BANNER1, TEXT_BLIND1, TEXT_BLOOD1, TEXT_ANVIL1, TEXT_WARLRD1, TEXT_KING1, TEXT_POISON1, TEXT_BONE1, TEXT_VILE2, TEXT_GRAVE6, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_DRUNK*/ { TEXT_INFRA8, TEXT_MUSH7, TEXT_NONE, TEXT_NONE, TEXT_VEIL6, TEXT_NONE, TEXT_BUTCH6, TEXT_BANNER7, TEXT_BLIND6, TEXT_BLOOD6, TEXT_ANVIL8, TEXT_WARLRD6, TEXT_KING8, TEXT_POISON8, TEXT_BONE6, TEXT_VILE10, TEXT_GRAVE7, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_WITCH*/ { TEXT_INFRA9, TEXT_MUSH9, TEXT_NONE, TEXT_NONE, TEXT_VEIL7, TEXT_NONE, TEXT_BUTCH7, TEXT_BANNER8, TEXT_BLIND7, TEXT_BLOOD7, TEXT_ANVIL9, TEXT_WARLRD7, TEXT_KING9, TEXT_POISON9, TEXT_BONE7, TEXT_VILE11, TEXT_GRAVE1, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_BMAID*/ { TEXT_INFRA4, TEXT_MUSH5, TEXT_NONE, TEXT_NONE, TEXT_VEIL4, TEXT_NONE, TEXT_BUTCH4, TEXT_BANNER5, TEXT_BLIND4, TEXT_BLOOD4, TEXT_ANVIL4, TEXT_WARLRD4, TEXT_KING6, TEXT_POISON6, TEXT_BONE4, TEXT_VILE8, TEXT_GRAVE8, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_PEGBOY*/ { TEXT_INFRA10, TEXT_MUSH13, TEXT_NONE, TEXT_NONE, TEXT_VEIL8, TEXT_NONE, TEXT_BUTCH8, TEXT_BANNER9, TEXT_BLIND8, TEXT_BLOOD8, TEXT_ANVIL10, TEXT_WARLRD8, TEXT_KING10, TEXT_POISON10, TEXT_BONE8, TEXT_VILE12, TEXT_GRAVE9, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_COW*/ { TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_FARMER*/ { TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_GIRL*/ { TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, /*TOWN_COWFARM*/ { TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE, TEXT_NONE }, // clang-format on }; void InitTowners() { assert(CowCels == nullptr); CowCels = LoadFileInMem("Towners\\Animals\\Cow.CEL"); int i = 0; for (const auto &townerInit : TownerInitList) { switch (townerInit.type) { case TOWN_DEADGUY: if (quests[Q_BUTCHER]._qactive == QUEST_NOTAVAIL || quests[Q_BUTCHER]._qactive == QUEST_DONE) continue; break; case TOWN_FARMER: if (!gbIsHellfire || sgGameInitInfo.bCowQuest) continue; break; case TOWN_COWFARM: if (!gbIsHellfire || !sgGameInitInfo.bCowQuest || quests[Q_FARMER]._qactive == 10) continue; break; case TOWN_GIRL: if (!gbIsHellfire || !sgGameInitInfo.bTheoQuest || !plr->_pLvlVisited[17]) continue; break; default: break; } InitTownerInfo(i, townerInit); i++; } } void FreeTownerGFX() { for (auto &towner : towners) { towner._tNData = nullptr; } CowCels = nullptr; } void ProcessTowners() { for (auto &towner : towners) { if (towner._ttype == TOWN_DEADGUY) { TownDead(towner); } towner._tAnimCnt++; if (towner._tAnimCnt < towner._tAnimDelay) { continue; } towner._tAnimCnt = 0; if (towner.animOrderSize > 0) { towner._tAnimFrameCnt++; if (towner._tAnimFrameCnt > towner.animOrderSize - 1) towner._tAnimFrameCnt = 0; towner._tAnimFrame = towner.animOrder[towner._tAnimFrameCnt]; continue; } towner._tAnimFrame++; if (towner._tAnimFrame > towner._tAnimLen) towner._tAnimFrame = 1; } } void TalkToTowner(PlayerStruct &player, int t) { auto &towner = towners[t]; Point distance = player.position.tile - towner.position; if (abs(distance.x) >= 2 || abs(distance.y) >= 2) return; if (pcurs >= CURSOR_FIRSTITEM) { return; } towner.talk(player, towner); } } // namespace devilution