Questionable/Questionable/Data/QuestData.cs
2024-11-17 19:52:04 +01:00

355 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using LLib.GameData;
using Lumina.Excel.Sheets;
using Questionable.Model;
using Questionable.Model.Questing;
using Quest = Lumina.Excel.Sheets.Quest;
namespace Questionable.Data;
internal sealed class QuestData
{
public static readonly IReadOnlyList<QuestId> CrystalTowerQuests =
[new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)];
public static readonly IReadOnlyList<uint> TankRoleQuests = [136, 154, 178];
public static readonly IReadOnlyList<uint> HealerRoleQuests = [137, 155, 179];
public static readonly IReadOnlyList<uint> MeleeRoleQuests = [138, 156, 180];
public static readonly IReadOnlyList<uint> PhysicalRangedRoleQuests = [138, 157, 181];
public static readonly IReadOnlyList<uint> CasterRoleQuests = [139, 158, 182];
public static readonly IReadOnlyList<IReadOnlyList<uint>> AllRoleQuestChapters =
[
TankRoleQuests,
HealerRoleQuests,
MeleeRoleQuests,
PhysicalRangedRoleQuests,
CasterRoleQuests
];
public static readonly IReadOnlyList<QuestId> FinalShadowbringersRoleQuests =
[new(3248), new(3272), new(3278), new(3628)];
private readonly Dictionary<ElementId, IQuestInfo> _quests;
public QuestData(IDataManager dataManager)
{
Dictionary<uint, uint> questChapters =
dataManager.GetExcelSheet<QuestChapter>()!
.Where(x => x.RowId > 0 && x.Quest.RowId > 0)
.ToDictionary(x => x.Quest.RowId, x => x.Redo.RowId);
Dictionary<uint, byte> startingCities = new();
for (byte redoChapter = 1; redoChapter <= 3; ++redoChapter)
{
var questRedo = dataManager.GetExcelSheet<QuestRedo>().GetRow(redoChapter);
foreach (var quest in questRedo.QuestRedoParam.Where(x => x.Quest.IsValid))
startingCities[quest.Quest.RowId] = redoChapter;
}
List<IQuestInfo> quests =
[
..dataManager.GetExcelSheet<Quest>()
.Where(x => x.RowId > 0)
.Where(x => x.IssuerLocation.RowId > 0)
.Select(x => new QuestInfo(x, questChapters.GetValueOrDefault(x.RowId),
startingCities.GetValueOrDefault(x.RowId)))
.Where(x => x.QuestId.Value != 1428),
..dataManager.GetExcelSheet<SatisfactionNpc>()
.Where(x => x is { RowId: > 0, Npc.RowId: > 0 })
.Select(x => new SatisfactionSupplyInfo(x)),
..dataManager.GetExcelSheet<Leve>()
.Where(x => x.RowId > 0)
.Where(x => x.LevelLevemete.RowId != 0)
.Select(x => new LeveInfo(x)),
];
_quests = quests.ToDictionary(x => x.QuestId, x => x);
// workaround because the game doesn't require completion of the CT questline through normal means
AddPreviousQuest(new QuestId(425), new QuestId(495));
// "In order to undertake this quest" [...]
const int mountaintopDiplomacy = 1619;
const int inscrutableTastes = 2095;
const int tideGoesIn = 2490;
const int firstOfMany = 2534;
const int achtIaOrmhInn = 3320;
AddPreviousQuest(new QuestId(1480), new QuestId(2373));
AddPreviousQuest(new QuestId(1717), new QuestId(mountaintopDiplomacy));
AddPreviousQuest(new QuestId(2088), new QuestId(mountaintopDiplomacy));
AddPreviousQuest(new QuestId(2062), new QuestId(1617));
AddPreviousQuest(new QuestId(2063), new QuestId(mountaintopDiplomacy));
AddPreviousQuest(new QuestId(2257), new QuestId(1655));
AddPreviousQuest(new QuestId(2608), new QuestId(firstOfMany));
AddPreviousQuest(new QuestId(2600), new QuestId(2466));
AddPreviousQuest(new QuestId(2622), new QuestId(tideGoesIn));
AddPreviousQuest(new QuestId(2624), new QuestId(firstOfMany));
AddPreviousQuest(new QuestId(2898), new QuestId(tideGoesIn));
AddPreviousQuest(new QuestId(2974), new QuestId(2491));
AddPreviousQuest(new QuestId(2975), new QuestId(2630));
AddPreviousQuest(new QuestId(2912), new QuestId(tideGoesIn));
AddPreviousQuest(new QuestId(2914), new QuestId(2537));
AddPreviousQuest(new QuestId(2919), new QuestId(2455));
AddPreviousQuest(new QuestId(2952), new QuestId(2518));
AddPreviousQuest(new QuestId(2904), new QuestId(2503));
AddPreviousQuest(new QuestId(3038), new QuestId(firstOfMany));
AddPreviousQuest(new QuestId(3087), new QuestId(100));
AddPreviousQuest(new QuestId(3246), new QuestId(3314));
AddPreviousQuest(new QuestId(3247), new QuestId(achtIaOrmhInn));
AddPreviousQuest(new QuestId(3270), new QuestId(3333));
AddPreviousQuest(new QuestId(3271), new QuestId(3634));
AddPreviousQuest(new QuestId(3264), new QuestId(2247));
AddPreviousQuest(new QuestId(3253), new QuestId(2247));
AddPreviousQuest(new QuestId(3254), new QuestId(2537));
AddPreviousQuest(new QuestId(3228), new QuestId(achtIaOrmhInn));
AddPreviousQuest(new QuestId(3234), new QuestId(achtIaOrmhInn));
AddPreviousQuest(new QuestId(3237), new QuestId(achtIaOrmhInn));
AddPreviousQuest(new QuestId(3238), new QuestId(3634));
AddPreviousQuest(new QuestId(3240), new QuestId(achtIaOrmhInn));
AddPreviousQuest(new QuestId(3241), new QuestId(3648));
AddPreviousQuest(new QuestId(3628), new QuestId(3301));
AddPreviousQuest(new QuestId(3655), new QuestId(inscrutableTastes));
AddPreviousQuest(new QuestId(3771), new QuestId(495));
AddPreviousQuest(new QuestId(4068), new QuestId(1658));
AddPreviousQuest(new QuestId(4078), new QuestId(1583));
AddPreviousQuest(new QuestId(4150), new QuestId(4417));
AddPreviousQuest(new QuestId(4155), new QuestId(4383));
AddPreviousQuest(new QuestId(4156), new QuestId(3326));
AddPreviousQuest(new QuestId(4158), new QuestId(4434));
AddPreviousQuest(new QuestId(4159), new QuestId(4464));
AddPreviousQuest(new QuestId(4163), new QuestId(4398));
AddPreviousQuest(new QuestId(4165), new QuestId(4438));
AddPreviousQuest(new QuestId(4473), new QuestId(inscrutableTastes));
AddPreviousQuest(new QuestId(4650), new QuestId(2374));
AddPreviousQuest(new QuestId(4662), new QuestId(3166));
AddPreviousQuest(new QuestId(4761), new QuestId(4032));
AddPreviousQuest(new QuestId(4812), new QuestId(4750));
AddPreviousQuest(new QuestId(4851), new QuestId(2446));
AddPreviousQuest(new QuestId(4856), new QuestId(1669));
AddPreviousQuest(new QuestId(4857), new QuestId(2553));
AddPreviousQuest(new QuestId(4979), new QuestId(4896));
AddPreviousQuest(new QuestId(4980), new QuestId(4911));
AddPreviousQuest(new QuestId(4985), new QuestId(4903));
AddPreviousQuest(new QuestId(4987), new QuestId(4912));
AddPreviousQuest(new QuestId(4988), new QuestId(4942));
AddPreviousQuest(new QuestId(4992), new QuestId(4912));
AddPreviousQuest(new QuestId(4999), new QuestId(4908));
AddPreviousQuest(new QuestId(4966), new QuestId(inscrutableTastes));
AddPreviousQuest(new QuestId(5000), new QuestId(4908));
AddPreviousQuest(new QuestId(5001), new QuestId(4912));
// "In order to proceed with this quest" [...]
/* my little chocobo
AddPreviousQuest(new QuestId(1036), new QuestId());
AddPreviousQuest(new QuestId(1663), new QuestId());
AddPreviousQuest(new QuestId(3771), new QuestId());
AddPreviousQuest(new QuestId(4521), new QuestId());
*/
/* only applicable for fishers
const int spearfishing = 2922;
AddPreviousQuest(new QuestId(3811), new QuestId(spearfishing));
AddPreviousQuest(new QuestId(3812), new QuestId(spearfishing));
AddPreviousQuest(new QuestId(3817), new QuestId(spearfishing));
AddPreviousQuest(new QuestId(3818), new QuestId(spearfishing));
AddPreviousQuest(new QuestId(3821), new QuestId(spearfishing));
AddPreviousQuest(new QuestId(3833), new QuestId(spearfishing));
*/
// initial city quests are side quests
// unclear if 470 can be started as the required quest isn't available anymore
ushort[] limsaSideQuests =
[107, 111, 112, 122, 663, 475, 472, 476, 470, 473, 474, 477, 486, 478, 479, 487, 59, 400, 401, 693, 405];
foreach (var questId in limsaSideQuests)
((QuestInfo)_quests[new QuestId(questId)]).StartingCity = 1;
ushort[] gridaniaQuests =
[39, 1, 32, 34, 37, 172, 127, 130, 60, 220, 378];
foreach (var questId in gridaniaQuests)
((QuestInfo)_quests[new QuestId(questId)]).StartingCity = 2;
ushort[] uldahSideQuests =
[594, 389, 390, 321, 304, 322, 388, 308, 326, 1429, 58, 687, 341, 504, 531, 506, 530, 573, 342, 505];
foreach (var questId in uldahSideQuests)
((QuestInfo)_quests[new QuestId(questId)]).StartingCity = 3;
// follow-up quests to picking a GC
AddGcFollowUpQuests();
}
private void AddPreviousQuest(QuestId questToUpdate, QuestId requiredQuestId)
{
QuestInfo quest = (QuestInfo)_quests[questToUpdate];
quest.AddPreviousQuest(new PreviousQuestInfo(requiredQuestId));
}
private void AddGcFollowUpQuests()
{
QuestId[] questIds = [new(683), new(684), new(685)];
foreach (QuestId questId in questIds)
{
QuestInfo quest = (QuestInfo)_quests[questId];
quest.AddQuestLocks(EQuestJoin.AtLeastOne, questIds.Where(x => x != questId).ToArray());
}
}
public IQuestInfo GetQuestInfo(ElementId elementId)
{
return _quests[elementId] ?? throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool TryGetQuestInfo(ElementId elementId, [NotNullWhen(true)] out IQuestInfo? questInfo)
{
return _quests.TryGetValue(elementId, out questInfo);
}
public List<IQuestInfo> GetAllByIssuerDataId(uint targetId)
{
return _quests.Values
.Where(x => x.IssuerDataId == targetId)
.ToList();
}
public bool IsIssuerOfAnyQuest(uint targetId) => _quests.Values.Any(x => x.IssuerDataId == targetId);
public List<IQuestInfo> GetAllByJournalGenre(uint journalGenre)
{
return _quests.Values
.Where(x => x is QuestInfo { IsSeasonalEvent: false } or not QuestInfo)
.Where(x => x.JournalGenre == journalGenre)
.OrderBy(x => x.SortKey)
.ThenBy(x => x.QuestId)
.ToList();
}
public List<QuestInfo> GetClassJobQuests(EClassJob classJob)
{
List<uint> chapterIds = classJob switch
{
EClassJob.Adventurer => throw new ArgumentOutOfRangeException(nameof(classJob)),
// ARR
EClassJob.Gladiator => [63],
EClassJob.Paladin => [72, 73, 74],
EClassJob.Marauder => [64],
EClassJob.Warrior => [76, 77, 78],
EClassJob.Conjurer => [65],
EClassJob.WhiteMage => [86, 87, 88],
EClassJob.Arcanist => [66],
EClassJob.Summoner => [127, 128, 129],
EClassJob.Scholar => [90, 91, 92],
EClassJob.Pugilist => [67],
EClassJob.Monk => [98, 99, 100],
EClassJob.Lancer => [68],
EClassJob.Dragoon => [102, 103, 104],
EClassJob.Rogue => [69],
EClassJob.Ninja => [106, 107, 108],
EClassJob.Archer => [70],
EClassJob.Bard => [113, 114, 115],
EClassJob.Thaumaturge => [71],
EClassJob.BlackMage => [123, 124, 125],
// HW
EClassJob.DarkKnight => [80, 81, 82],
EClassJob.Astrologian => [94, 95, 96],
EClassJob.Machinist => [117, 118, 119],
// SB
EClassJob.Samurai => [110, 111],
EClassJob.RedMage => [131, 132],
EClassJob.BlueMage => [134, 135, 146],
// ShB
EClassJob.Gunbreaker => [84],
EClassJob.Dancer => [121],
// EW
EClassJob.Sage => [152],
EClassJob.Reaper => [153],
// DT
EClassJob.Viper => [176],
EClassJob.Pictomancer => [177],
// Crafter
EClassJob.Alchemist => [48, 49, 50],
EClassJob.Armorer => [36, 37, 38],
EClassJob.Blacksmith => [33, 34, 35],
EClassJob.Carpenter => [30, 31, 32],
EClassJob.Culinarian => [51, 52, 53],
EClassJob.Goldsmith => [39, 40, 41],
EClassJob.Leatherworker => [42, 43, 44],
EClassJob.Weaver => [45, 46, 47],
// Gatherer
EClassJob.Miner => [54, 55, 56],
EClassJob.Botanist => [57, 58, 59],
EClassJob.Fisher => [60, 61, 62],
_ => throw new ArgumentOutOfRangeException(nameof(classJob)),
};
chapterIds.AddRange(classJob switch
{
_ when classJob.IsTank() => TankRoleQuests,
_ when classJob.IsHealer() => HealerRoleQuests,
_ when classJob.IsMelee() => MeleeRoleQuests,
_ when classJob.IsPhysicalRanged() => PhysicalRangedRoleQuests,
_ when classJob.IsCaster() && classJob != EClassJob.BlueMage => CasterRoleQuests,
_ => []
});
return GetQuestsInNewGamePlusChapters(chapterIds);
}
private List<QuestInfo> GetQuestsInNewGamePlusChapters(List<uint> chapterIds)
{
return _quests.Values
.Where(x => x is QuestInfo)
.Cast<QuestInfo>()
.Where(x => chapterIds.Contains(x.NewGamePlusChapter))
.ToList();
}
public List<QuestId> GetLockedClassQuests()
{
EClassJob startingClass;
unsafe
{
var playerState = PlayerState.Instance();
if (playerState != null)
startingClass = (EClassJob)playerState->FirstClass;
else
startingClass = EClassJob.Adventurer;
}
if (startingClass == EClassJob.Adventurer)
return [];
// If you start the game as another class, you get:
// - "So you want to be a XX"
// - "Way of the XX" (depends on "So you want to be a XX")
// - "My First XX"
// If you start the game with this class, you get:
// - "Way of the XX" (no preconditions)
// In both cases, the level 10 quests are different
List<List<ushort>> startingClassQuests =
[
startingClass == EClassJob.Gladiator ? [177, 285, 286, 288] : [253, 261],
startingClass == EClassJob.Pugilist ? [178, 532, 553, 698] : [533, 555],
startingClass == EClassJob.Marauder ? [179, 310, 312, 315] : [311, 314],
startingClass == EClassJob.Lancer ? [180, 132, 218, 143] : [23, 35],
startingClass == EClassJob.Archer ? [181, 131, 219, 134] : [21, 67],
startingClass == EClassJob.Conjurer ? [182, 133, 211, 147] : [22, 91],
startingClass == EClassJob.Thaumaturge ? [183, 344, 346, 349] : [345, 348],
startingClass == EClassJob.Arcanist ? [451, 452, 454, 457] : [453, 456],
];
return startingClassQuests.SelectMany(x => x).Select(x => new QuestId(x)).ToList();
}
}