diff --git a/Source/quests.cpp b/Source/quests.cpp index 42d4ba6ff..6b1dd9b8d 100644 --- a/Source/quests.cpp +++ b/Source/quests.cpp @@ -38,42 +38,54 @@ int ReturnLevel; /** Contains the data related to each quest_id. */ QuestDataStruct QuestData[] = { // clang-format off - // _qdlvl, _qdmultlvl, _qlvlt, _qdrnd, _qslvl, isSinglePlayerOnly, _qdmsg, _qlstr - { 5, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_INFRA5, N_( /* TRANSLATORS: Quest Name Block */ "The Magic Rock") }, - { 9, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_MUSH8, N_("Black Mushroom") }, - { 4, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_GARBUD1, N_("Gharbad The Weak") }, - { 8, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_ZHAR1, N_("Zhar the Mad") }, - { 14, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_VEIL9, "Lachdanan" }, - { 15, -1, DTYPE_NONE, 100, SL_NONE, false, TEXT_VILE3, "Diablo" }, - { 2, 2, DTYPE_NONE, 100, SL_NONE, false, TEXT_BUTCH9, N_("The Butcher") }, - { 4, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_BANNER2, N_("Ogden's Sign") }, - { 7, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_BLINDING, N_("Halls of the Blind") }, - { 5, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_BLOODY, N_("Valor") }, - { 10, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_ANVIL5, N_("Anvil of Fury") }, - { 13, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_BLOODWAR, N_("Warlord of Blood") }, - { 3, 3, DTYPE_CATHEDRAL, 100, SL_SKELKING, false, TEXT_KING2, N_("The Curse of King Leoric") }, - { 2, -1, DTYPE_CAVES, 100, SL_POISONWATER, true, TEXT_POISON3, N_("Poisoned Water Supply") }, - { 6, -1, DTYPE_CATACOMBS, 100, SL_BONECHAMB, true, TEXT_BONER, N_("The Chamber of Bone") }, - { 15, 15, DTYPE_CATHEDRAL, 100, SL_VILEBETRAYER, false, TEXT_VILE1, N_("Archbishop Lazarus") }, - { 17, 17, DTYPE_NONE, 100, SL_NONE, false, TEXT_GRAVE7, N_("Grave Matters") }, - { 9, 9, DTYPE_NONE, 100, SL_NONE, false, TEXT_FARMER1, N_("Farmer's Orchard") }, - { 17, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_GIRL2, N_("Little Girl") }, - { 19, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_TRADER, N_("Wandering Trader") }, - { 17, 17, DTYPE_NONE, 100, SL_NONE, false, TEXT_DEFILER1, N_("The Defiler") }, - { 21, 21, DTYPE_NONE, 100, SL_NONE, false, TEXT_NAKRUL1, "Na-Krul" }, - { 21, -1, DTYPE_NONE, 100, SL_NONE, true, TEXT_CORNSTN, N_("Cornerstone of the World") }, - { 9, 9, DTYPE_NONE, 100, SL_NONE, false, TEXT_JERSEY4, N_( /* TRANSLATORS: Quest Name Block end*/ "The Jersey's Jersey") }, + // _qdlvl, _qdmultlvl, _qlvlt, bookOrder, _qdrnd, _qslvl, isSinglePlayerOnly, _qdmsg, _qlstr + { 5, -1, DTYPE_NONE, 5, 100, SL_NONE, true, TEXT_INFRA5, N_( /* TRANSLATORS: Quest Name Block */ "The Magic Rock") }, + { 9, -1, DTYPE_NONE, 10, 100, SL_NONE, true, TEXT_MUSH8, N_("Black Mushroom") }, + { 4, -1, DTYPE_NONE, 3, 100, SL_NONE, true, TEXT_GARBUD1, N_("Gharbad The Weak") }, + { 8, -1, DTYPE_NONE, 9, 100, SL_NONE, true, TEXT_ZHAR1, N_("Zhar the Mad") }, + { 14, -1, DTYPE_NONE, 21, 100, SL_NONE, true, TEXT_VEIL9, "Lachdanan" }, + { 15, -1, DTYPE_NONE, 23, 100, SL_NONE, false, TEXT_VILE3, "Diablo" }, + { 2, 2, DTYPE_NONE, 0, 100, SL_NONE, false, TEXT_BUTCH9, N_("The Butcher") }, + { 4, -1, DTYPE_NONE, 4, 100, SL_NONE, true, TEXT_BANNER2, N_("Ogden's Sign") }, + { 7, -1, DTYPE_NONE, 8, 100, SL_NONE, true, TEXT_BLINDING, N_("Halls of the Blind") }, + { 5, -1, DTYPE_NONE, 6, 100, SL_NONE, true, TEXT_BLOODY, N_("Valor") }, + { 10, -1, DTYPE_NONE, 11, 100, SL_NONE, true, TEXT_ANVIL5, N_("Anvil of Fury") }, + { 13, -1, DTYPE_NONE, 20, 100, SL_NONE, true, TEXT_BLOODWAR, N_("Warlord of Blood") }, + { 3, 3, DTYPE_CATHEDRAL, 2, 100, SL_SKELKING, false, TEXT_KING2, N_("The Curse of King Leoric") }, + { 2, -1, DTYPE_CAVES, 1, 100, SL_POISONWATER, true, TEXT_POISON3, N_("Poisoned Water Supply") }, + { 6, -1, DTYPE_CATACOMBS, 7, 100, SL_BONECHAMB, true, TEXT_BONER, N_("The Chamber of Bone") }, + { 15, 15, DTYPE_CATHEDRAL, 22, 100, SL_VILEBETRAYER, false, TEXT_VILE1, N_("Archbishop Lazarus") }, + { 17, 17, DTYPE_NONE, 17, 100, SL_NONE, false, TEXT_GRAVE7, N_("Grave Matters") }, + { 9, 9, DTYPE_NONE, 12, 100, SL_NONE, false, TEXT_FARMER1, N_("Farmer's Orchard") }, + { 17, -1, DTYPE_NONE, 14, 100, SL_NONE, true, TEXT_GIRL2, N_("Little Girl") }, + { 19, -1, DTYPE_NONE, 16, 100, SL_NONE, true, TEXT_TRADER, N_("Wandering Trader") }, + { 17, 17, DTYPE_NONE, 15, 100, SL_NONE, false, TEXT_DEFILER1, N_("The Defiler") }, + { 21, 21, DTYPE_NONE, 19, 100, SL_NONE, false, TEXT_NAKRUL1, "Na-Krul" }, + { 21, -1, DTYPE_NONE, 18, 100, SL_NONE, true, TEXT_CORNSTN, N_("Cornerstone of the World") }, + { 9, 9, DTYPE_NONE, 13, 100, SL_NONE, false, TEXT_JERSEY4, N_( /* TRANSLATORS: Quest Name Block end*/ "The Jersey's Jersey") }, // clang-format on }; namespace { -int qtopline; -int qline; -quest_id qlist[MAXQUESTS]; -int numqlines; int WaterDone; +/** Indices of quests to display in quest log window. `fistfinishedEntry` are active quests the rest are completed */ +quest_id qlist[MAXQUESTS]; +/** Overall number of qlist entries */ +int qlistCnt; +/** First (nonselectable) finished quest in list */ +int firstFinishedEntry; +/** Currently selected quest list item */ +int selectedEntry; + +constexpr Rectangle panelInnerRect { { 32, 26 }, { 280, 300 } }; +constexpr int lineHeight = 12; +constexpr int maxSpacing = lineHeight * 2; +int topY; +int lineSpacing; +int act2finSpacing; + /** * Specifies a delta in X-coordinates from the quest entrance for * which the hover text of the cursor will be visible. @@ -246,16 +258,32 @@ void DrawBlood(int x, int y) } } -void PrintQLString(const Surface &out, int x, int line, const char *str) +int QuestLogMouseToEntry() +{ + Rectangle innerArea = panelInnerRect; + innerArea.position += Displacement(LeftPanel.position.x, LeftPanel.position.y); + if (!innerArea.Contains(MousePosition) || (qlistCnt == 0)) + return -1; + int y = MousePosition.y - innerArea.position.y; + for (int i = 0; i < firstFinishedEntry; i++) { + if ((y >= topY + i * lineSpacing) + && (y < topY + i * lineSpacing + lineHeight)) { + return i; + } + } + return -1; +} + +void PrintQLString(const Surface &out, int x, int y, const char *str, bool marked, bool disabled = false) { int width = GetLineWidth(str); int sx = x + std::max((257 - width) / 2, 0); - int sy = line * 12 + 44; - if (qline == line) { + int sy = y + lineHeight; //seems that DrawString y is the text base line -> so add a lines height + if (marked) { CelDrawTo(out, GetPanelPosition(UiPanels::Quest, { sx - 20, sy + 1 }), *pSPentSpn2Cels, PentSpn2Spin()); } - DrawString(out, str, { GetPanelPosition(UiPanels::Quest, { sx, sy }), { 257, 0 } }, UiFlags::ColorSilver); - if (qline == line) { + DrawString(out, str, { GetPanelPosition(UiPanels::Quest, { sx, sy }), { 257, 0 } }, disabled ? UiFlags::ColorGold : UiFlags::ColorSilver); + if (marked) { CelDrawTo(out, GetPanelPosition(UiPanels::Quest, { sx + width + 7, sy + 1 }), *pSPentSpn2Cels, PentSpn2Spin()); } } @@ -721,45 +749,82 @@ void ResyncQuests() void DrawQuestLog(const Surface &out) { - DrawString(out, _("Quest Log"), { GetPanelPosition(UiPanels::Quest, { 32, 44 }), { 257, 0 } }, UiFlags::AlignCenter); + int l = QuestLogMouseToEntry(); + if (l >= 0) { + selectedEntry = l; + } + const auto x = panelInnerRect.position.x; CelDrawTo(out, GetPanelPosition(UiPanels::Quest, { 0, 351 }), *pQLogCel, 1); - int line = qtopline; - for (int i = 0; i < numqlines; i++) { - PrintQLString(out, 32, line, _(QuestData[qlist[i]]._qlstr)); - line += 2; + int y = panelInnerRect.position.y + topY; + for (int i = 0; i < qlistCnt; i++) { + if (i == firstFinishedEntry) { + y += act2finSpacing; + } + PrintQLString(out, x, y, _(QuestData[qlist[i]]._qlstr), i == selectedEntry, i >= firstFinishedEntry); + y += lineSpacing; } - PrintQLString(out, 32, 22, _("Close Quest Log")); } void StartQuestlog() { - numqlines = 0; + + auto sortQuestIdx = [](int a, int b) { + return QuestData[a].questBookOrder < QuestData[b].questBookOrder; + }; + + qlistCnt = 0; for (auto &quest : Quests) { if (quest._qactive == QUEST_ACTIVE && quest._qlog) { - qlist[numqlines] = quest._qidx; - numqlines++; + qlist[qlistCnt] = quest._qidx; + qlistCnt++; } } - if (numqlines > 5) { - qtopline = 5 - (numqlines / 2); - } else { - qtopline = 8; + firstFinishedEntry = qlistCnt; + for (auto &quest : Quests) { + if (quest._qactive == QUEST_DONE || quest._qactive == QUEST_HIVE_DONE) { + qlist[qlistCnt] = quest._qidx; + qlistCnt++; + } } - qline = 22; - if (numqlines != 0) - qline = qtopline; + + std::sort(&qlist[0], &qlist[firstFinishedEntry], sortQuestIdx); + std::sort(&qlist[firstFinishedEntry], &qlist[qlistCnt], sortQuestIdx); + + bool twoBlocks = firstFinishedEntry != 0 && firstFinishedEntry < qlistCnt; + + topY = 0; + act2finSpacing = !twoBlocks ? 0 : lineHeight / 2; + + int overallMinHeight = qlistCnt * lineHeight + act2finSpacing; + int space = panelInnerRect.size.height; + + if (qlistCnt > 0) { + int additionalSpace = space - overallMinHeight; + int addLineSpacing = additionalSpace / qlistCnt; + addLineSpacing = std::min(maxSpacing - lineHeight, addLineSpacing); + lineSpacing = lineHeight + addLineSpacing; + if (twoBlocks) { + int additionalSepSpace = additionalSpace - (addLineSpacing * qlistCnt); + additionalSepSpace = std::min(lineHeight, additionalSepSpace); + act2finSpacing = std::max(4, additionalSepSpace); + } + + int overallHeight = qlistCnt * lineSpacing + act2finSpacing; + topY += (space - overallHeight) / 2; + } + + selectedEntry = firstFinishedEntry == 0 ? -1 : 0; QuestLogIsOpen = true; } void QuestlogUp() { - if (numqlines != 0) { - if (qline == qtopline) { - qline = 22; - } else if (qline == 22) { - qline = qtopline + 2 * numqlines - 2; - } else { - qline -= 2; + if (firstFinishedEntry == 0) { + selectedEntry = -1; + } else { + selectedEntry--; + if (selectedEntry < 0) { + selectedEntry = firstFinishedEntry - 1; } PlaySFX(IS_TITLEMOV); } @@ -767,13 +832,12 @@ void QuestlogUp() void QuestlogDown() { - if (numqlines != 0) { - if (qline == 22) { - qline = qtopline; - } else if (qline == qtopline + 2 * numqlines - 2) { - qline = 22; - } else { - qline += 2; + if (firstFinishedEntry == 0) { + selectedEntry = -1; + } else { + selectedEntry++; + if (selectedEntry == firstFinishedEntry) { + selectedEntry = 0; } PlaySFX(IS_TITLEMOV); } @@ -782,25 +846,15 @@ void QuestlogDown() void QuestlogEnter() { PlaySFX(IS_TITLSLCT); - if (numqlines != 0 && qline != 22) - InitQTextMsg(Quests[qlist[(qline - qtopline) / 2]]._qmsg); + if (qlistCnt != 0 && selectedEntry < firstFinishedEntry) + InitQTextMsg(Quests[qlist[selectedEntry]]._qmsg); QuestLogIsOpen = false; } void QuestlogESC() { - Rectangle innerArea = { { LeftPanel.position.x + 32, LeftPanel.position.y + 32 }, { 288 - 32, 308 - 32 } }; - int y = (MousePosition.y - LeftPanel.position.y - 32) / 12; - if (!innerArea.Contains(MousePosition)) - return; - for (int i = 0; i < numqlines; i++) { - if (y == qtopline + 2 * i) { - qline = y; - QuestlogEnter(); - } - } - if (y == 22) { - qline = 22; + int l = QuestLogMouseToEntry(); + if (l != -1) { QuestlogEnter(); } } diff --git a/Source/quests.h b/Source/quests.h index 931516834..3b6b088f8 100644 --- a/Source/quests.h +++ b/Source/quests.h @@ -63,6 +63,7 @@ struct QuestDataStruct { uint8_t _qdlvl; int8_t _qdmultlvl; dungeon_type _qlvlt; + int8_t questBookOrder; uint8_t _qdrnd; _setlevels _qslvl; bool isSinglePlayerOnly;