Server/project/src/controllers/QuestController.ts

955 lines
37 KiB
TypeScript
Raw Normal View History

2023-03-03 15:23:46 +00:00
import { inject, injectable } from "tsyringe";
import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { QuestConditionHelper } from "@spt-aki/helpers/QuestConditionHelper";
import { QuestHelper } from "@spt-aki/helpers/QuestHelper";
import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { IQuestStatus } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { AvailableForConditions, IQuest, Reward } from "@spt-aki/models/eft/common/tables/IQuest";
import { IPmcDataRepeatableQuest, IRepeatableQuest } from "@spt-aki/models/eft/common/tables/IRepeatableQuests";
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse";
import { IAcceptQuestRequestData } from "@spt-aki/models/eft/quests/IAcceptQuestRequestData";
import { ICompleteQuestRequestData } from "@spt-aki/models/eft/quests/ICompleteQuestRequestData";
import { IFailQuestRequestData } from "@spt-aki/models/eft/quests/IFailQuestRequestData";
import { IHandoverQuestRequestData } from "@spt-aki/models/eft/quests/IHandoverQuestRequestData";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { MessageType } from "@spt-aki/models/enums/MessageType";
import { QuestStatus } from "@spt-aki/models/enums/QuestStatus";
import { SeasonalEventType } from "@spt-aki/models/enums/SeasonalEventType";
import { IQuestConfig } from "@spt-aki/models/spt/config/IQuestConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder";
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { LocaleService } from "@spt-aki/services/LocaleService";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { MailSendService } from "@spt-aki/services/MailSendService";
import { PlayerService } from "@spt-aki/services/PlayerService";
import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService";
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { TimeUtil } from "@spt-aki/utils/TimeUtil";
2023-03-03 15:23:46 +00:00
@injectable()
export class QuestController
{
protected questConfig: IQuestConfig;
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
2023-03-03 15:23:46 +00:00
@inject("HttpResponseUtil") protected httpResponseUtil: HttpResponseUtil,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
@inject("MailSendService") protected mailSendService: MailSendService,
2023-03-03 15:23:46 +00:00
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("TraderHelper") protected traderHelper: TraderHelper,
2023-03-03 15:23:46 +00:00
@inject("QuestHelper") protected questHelper: QuestHelper,
@inject("QuestConditionHelper") protected questConditionHelper: QuestConditionHelper,
@inject("PlayerService") protected playerService: PlayerService,
@inject("LocaleService") protected localeService: LocaleService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
2023-03-03 15:23:46 +00:00
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer,
2023-03-03 15:23:46 +00:00
)
{
this.questConfig = this.configServer.getConfig(ConfigTypes.QUEST);
}
/**
2023-07-15 11:00:35 +01:00
* Handle client/quest/list
2023-03-03 15:23:46 +00:00
* Get all quests visible to player
* Exclude quests with incomplete preconditions (level/loyalty)
* @param sessionID session id
* @returns array of IQuest
*/
public getClientQuests(sessionID: string): IQuest[]
{
2023-07-10 15:48:49 +01:00
const questsToShowPlayer: IQuest[] = [];
2023-03-03 15:23:46 +00:00
const allQuests = this.questHelper.getQuestsFromDb();
const profile: IPmcData = this.profileHelper.getPmcProfile(sessionID);
for (const quest of allQuests)
{
// Player already accepted the quest, show it regardless of status
const questInProfile = profile.Quests.find((x) => x.qid === quest._id);
if (questInProfile)
2023-03-03 15:23:46 +00:00
{
quest.sptStatus = questInProfile.status;
2023-07-10 15:48:49 +01:00
questsToShowPlayer.push(quest);
2023-03-03 15:23:46 +00:00
continue;
}
// Filter out bear quests for usec and vice versa
2023-03-03 15:23:46 +00:00
if (this.questIsForOtherSide(profile.Info.Side, quest._id))
{
continue;
}
if (!this.showEventQuestToPlayer(quest._id))
{
continue;
}
2023-03-03 15:23:46 +00:00
// Don't add quests that have a level higher than the user's
if (!this.playerLevelFulfillsQuestRequirement(quest, profile.Info.Level))
2023-03-03 15:23:46 +00:00
{
2023-07-10 15:48:49 +01:00
continue;
}
// Player can use trader mods then remove them, leaving quests behind
const trader = profile.TradersInfo[quest.traderId];
if (!trader)
{
this.logger.debug(`Unable to show quest: ${quest.QuestName} as its for a trader: ${quest.traderId} that no longer exists.`);
continue;
2023-03-03 15:23:46 +00:00
}
const questRequirements = this.questConditionHelper.getQuestConditions(quest.conditions.AvailableForStart);
const loyaltyRequirements = this.questConditionHelper.getLoyaltyConditions(
quest.conditions.AvailableForStart,
);
const standingRequirements = this.questConditionHelper.getStandingConditions(
quest.conditions.AvailableForStart,
);
2023-03-03 15:23:46 +00:00
// Quest has no conditions, standing or loyalty conditions, add to visible quest list
if (questRequirements.length === 0 && loyaltyRequirements.length === 0 && standingRequirements.length === 0)
2023-03-03 15:23:46 +00:00
{
quest.sptStatus = QuestStatus.AvailableForStart;
2023-07-10 15:48:49 +01:00
questsToShowPlayer.push(quest);
2023-03-03 15:23:46 +00:00
continue;
}
// Check the status of each quest condition, if any are not completed
// then this quest should not be visible
let haveCompletedPreviousQuest = true;
for (const conditionToFulfil of questRequirements)
2023-03-03 15:23:46 +00:00
{
// If the previous quest isn't in the user profile, it hasn't been completed or started
const prerequisiteQuest = profile.Quests.find((pq) => pq.qid === conditionToFulfil.target);
if (!prerequisiteQuest)
2023-03-03 15:23:46 +00:00
{
haveCompletedPreviousQuest = false;
break;
}
// Prereq does not have its status requirement fulfilled
if (!conditionToFulfil.status.includes(prerequisiteQuest.status))
2023-03-03 15:23:46 +00:00
{
haveCompletedPreviousQuest = false;
break;
2023-03-03 15:23:46 +00:00
}
// Has a wait timer
if (conditionToFulfil.availableAfter > 0)
{
// Compare current time to unlock time for previous quest
const previousQuestCompleteTime = prerequisiteQuest.statusTimers[prerequisiteQuest.status];
const unlockTime = previousQuestCompleteTime + conditionToFulfil.availableAfter;
if (unlockTime > this.timeUtil.getTimestamp())
{
this.logger.debug(
`Quest ${quest.QuestName} is locked for another ${
unlockTime - this.timeUtil.getTimestamp()
} seconds`,
);
}
}
}
2023-03-03 15:23:46 +00:00
// Previous quest not completed, skip
if (!haveCompletedPreviousQuest)
{
continue;
2023-03-03 15:23:46 +00:00
}
let passesLoyaltyRequirements = true;
for (const condition of loyaltyRequirements)
{
if (!this.questHelper.traderLoyaltyLevelRequirementCheck(condition, profile))
2023-03-03 15:23:46 +00:00
{
passesLoyaltyRequirements = false;
break;
}
}
let passesStandingRequirements = true;
for (const condition of standingRequirements)
2023-03-03 15:23:46 +00:00
{
if (!this.questHelper.traderStandingRequirementCheck(condition, profile))
{
passesStandingRequirements = false;
break;
}
}
if (haveCompletedPreviousQuest && passesLoyaltyRequirements && passesStandingRequirements)
{
quest.sptStatus = QuestStatus.AvailableForStart;
2023-07-10 15:48:49 +01:00
questsToShowPlayer.push(quest);
}
}
return questsToShowPlayer;
}
/**
* Does a provided quest have a level requirement equal to or below defined level
* @param quest Quest to check
* @param playerLevel level of player to test against quest
* @returns true if quest can be seen/accepted by player of defined level
*/
protected playerLevelFulfillsQuestRequirement(quest: IQuest, playerLevel: number): boolean
2023-07-10 15:48:49 +01:00
{
const levelConditions = this.questConditionHelper.getLevelConditions(quest.conditions.AvailableForStart);
if (levelConditions.length)
{
for (const levelCondition of levelConditions)
{
if (!this.questHelper.doesPlayerLevelFulfilCondition(playerLevel, levelCondition))
{
// Not valid, exit out
return false;
}
2023-03-03 15:23:46 +00:00
}
}
2023-07-10 15:48:49 +01:00
// All conditions passed / has no level requirement, valid
return true;
2023-03-03 15:23:46 +00:00
}
/**
* Should a quest be shown to the player in trader quest screen
* @param questId Quest to check
* @returns true = show to player
*/
protected showEventQuestToPlayer(questId: string): boolean
{
const isChristmasEventActive = this.seasonalEventService.christmasEventEnabled();
const isHalloweenEventActive = this.seasonalEventService.halloweenEventEnabled();
// Not christmas + quest is for christmas
if (
!isChristmasEventActive
&& this.seasonalEventService.isQuestRelatedToEvent(questId, SeasonalEventType.CHRISTMAS)
)
{
return false;
}
// Not halloween + quest is for halloween
if (
!isHalloweenEventActive
&& this.seasonalEventService.isQuestRelatedToEvent(questId, SeasonalEventType.HALLOWEEN)
)
{
return false;
}
// Should non-season event quests be shown to player
if (
!this.questConfig.showNonSeasonalEventQuests
&& this.seasonalEventService.isQuestRelatedToEvent(questId, SeasonalEventType.NONE)
)
{
return false;
}
return true;
}
2023-03-03 15:23:46 +00:00
/**
* Is the quest for the opposite side the player is on
* @param playerSide Player side (usec/bear)
* @param questId QuestId to check
2023-03-03 15:23:46 +00:00
*/
protected questIsForOtherSide(playerSide: string, questId: string): boolean
2023-03-03 15:23:46 +00:00
{
const isUsec = playerSide.toLowerCase() === "usec";
2023-03-03 15:23:46 +00:00
if (isUsec && this.questConfig.bearOnlyQuests.includes(questId))
{
// player is usec and quest is bear only, skip
return true;
}
if (!isUsec && this.questConfig.usecOnlyQuests.includes(questId))
{
// player is bear and quest is usec only, skip
return true;
}
return false;
}
/**
2023-07-15 11:00:35 +01:00
* Handle QuestAccept event
2023-03-03 15:23:46 +00:00
* Handle the client accepting a quest and starting it
* Send starting rewards if any to player and
* Send start notification if any to player
* @param pmcData Profile to update
* @param acceptedQuest Quest accepted
* @param sessionID Session id
* @returns Client response
2023-03-03 15:23:46 +00:00
*/
public acceptQuest(
pmcData: IPmcData,
acceptedQuest: IAcceptQuestRequestData,
sessionID: string,
): IItemEventRouterResponse
2023-03-03 15:23:46 +00:00
{
const acceptQuestResponse = this.eventOutputHolder.getOutput(sessionID);
2023-03-03 15:23:46 +00:00
// Does quest exist in profile
2023-11-30 09:36:28 +00:00
// Restarting a failed quest can mean quest exists in profile
const existingQuestStatus = pmcData.Quests.find((x) => x.qid === acceptedQuest.qid)
if (existingQuestStatus)
2023-03-03 15:23:46 +00:00
{
// Update existing
this.questHelper.resetQuestState(pmcData, QuestStatus.Started, acceptedQuest.qid);
2023-11-30 09:36:28 +00:00
// Need to send client an empty list of completedConditions (Unsure if this does anything)
acceptQuestResponse.profileChanges[sessionID].questsStatus.push(existingQuestStatus);
2023-03-03 15:23:46 +00:00
}
else
{
// Add new quest to server profile
const newQuest = this.questHelper.getQuestReadyForProfile(pmcData, QuestStatus.Started, acceptedQuest);
2023-03-03 15:23:46 +00:00
pmcData.Quests.push(newQuest);
}
// Create a dialog message for starting the quest.
// Note that for starting quests, the correct locale field is "description", not "startedMessageText".
const questFromDb = this.questHelper.getQuestFromDb(acceptedQuest.qid, pmcData);
2023-03-03 15:23:46 +00:00
// Get messageId of text to send to player as text message in game
const messageId = this.questHelper.getMessageIdForQuestStart(
questFromDb.startedMessageText,
questFromDb.description,
);
2023-11-30 09:36:28 +00:00
// Apply non-item rewards to profile + return item rewards
const startedQuestRewardItems = this.questHelper.applyQuestReward(
pmcData,
acceptedQuest.qid,
QuestStatus.Started,
sessionID,
acceptQuestResponse,
);
2023-11-30 09:36:28 +00:00
// Send started text + any starting reward items found above to player
this.mailSendService.sendLocalisedNpcMessageToPlayer(
sessionID,
this.traderHelper.getTraderById(questFromDb.traderId),
MessageType.QUEST_START,
messageId,
2023-11-30 09:36:28 +00:00
startedQuestRewardItems,
this.timeUtil.getHoursAsSeconds(this.questConfig.redeemTime),
);
2023-03-03 15:23:46 +00:00
2023-11-30 09:36:28 +00:00
// Having accepted new quest, look for newly unlocked quests and inform client of them
acceptQuestResponse.profileChanges[sessionID].quests.push(...this.questHelper
.getNewlyAccessibleQuestsWhenStartingQuest(acceptedQuest.qid, sessionID));
2023-03-03 15:23:46 +00:00
return acceptQuestResponse;
}
/**
* Handle the client accepting a repeatable quest and starting it
* Send starting rewards if any to player and
* Send start notification if any to player
* @param pmcData Profile to update with new quest
* @param acceptedQuest Quest being accepted
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public acceptRepeatableQuest(
pmcData: IPmcData,
acceptedQuest: IAcceptQuestRequestData,
sessionID: string,
): IItemEventRouterResponse
2023-03-03 15:23:46 +00:00
{
const acceptQuestResponse = this.eventOutputHolder.getOutput(sessionID);
// Create and store quest status object inside player profile
const newRepeatableQuest = this.questHelper.getQuestReadyForProfile(pmcData, QuestStatus.Started, acceptedQuest);
pmcData.Quests.push(newRepeatableQuest);
// Look for the generated quest cache in profile.RepeatableQuests
2023-03-03 15:23:46 +00:00
const repeatableQuestProfile = this.getRepeatableQuestFromProfile(pmcData, acceptedQuest);
if (!repeatableQuestProfile)
{
this.logger.error(
this.localisationService.getText(
"repeatable-accepted_repeatable_quest_not_found_in_active_quests",
acceptedQuest.qid,
),
);
2023-03-03 15:23:46 +00:00
throw new Error(this.localisationService.getText("repeatable-unable_to_accept_quest_see_log"));
}
// Some scav quests need to be added to scav profile for them to show up in-raid
if (
repeatableQuestProfile.side === "Scav"
&& ["PickUp", "Exploration", "Elimination"].includes(repeatableQuestProfile.type)
)
{
const fullProfile = this.profileHelper.getFullProfile(sessionID);
if (!fullProfile.characters.scav.Quests)
{
fullProfile.characters.scav.Quests = [];
}
fullProfile.characters.scav.Quests.push(newRepeatableQuest);
}
const repeatableSettings = pmcData.RepeatableQuests.find((x) =>
x.name === repeatableQuestProfile.sptRepatableGroupName
);
const change = {};
change[repeatableQuestProfile._id] = repeatableSettings.changeRequirement[repeatableQuestProfile._id];
const responseData: IPmcDataRepeatableQuest = {
id: repeatableSettings.id ?? this.questConfig.repeatableQuests.find((x) =>
x.name === repeatableQuestProfile.sptRepatableGroupName
).id,
name: repeatableSettings.name,
endTime: repeatableSettings.endTime,
changeRequirement: change,
activeQuests: [repeatableQuestProfile],
inactiveQuests: [],
};
if (!acceptQuestResponse.profileChanges[sessionID].repeatableQuests)
{
acceptQuestResponse.profileChanges[sessionID].repeatableQuests = []
}
acceptQuestResponse.profileChanges[sessionID].repeatableQuests.push(responseData);
2023-03-03 15:23:46 +00:00
return acceptQuestResponse;
}
/**
* Look for an accepted quest inside player profile, return matching
* @param pmcData Profile to search through
* @param acceptedQuest Quest to search for
* @returns IRepeatableQuest
*/
protected getRepeatableQuestFromProfile(pmcData: IPmcData, acceptedQuest: IAcceptQuestRequestData): IRepeatableQuest
{
for (const repeatableQuest of pmcData.RepeatableQuests)
2023-03-03 15:23:46 +00:00
{
const matchingQuest = repeatableQuest.activeQuests.find((x) => x._id === acceptedQuest.qid);
2023-03-21 14:22:45 +00:00
if (matchingQuest)
2023-03-03 15:23:46 +00:00
{
this.logger.debug(`Accepted repeatable quest ${acceptedQuest.qid} from ${repeatableQuest.name}`);
matchingQuest.sptRepatableGroupName = repeatableQuest.name;
2023-03-21 14:22:45 +00:00
return matchingQuest;
2023-03-03 15:23:46 +00:00
}
}
return undefined;
2023-03-03 15:23:46 +00:00
}
/**
2023-07-15 11:00:35 +01:00
* Handle QuestComplete event
2023-03-03 15:23:46 +00:00
* Update completed quest in profile
* Add newly unlocked quests to profile
2023-07-15 11:00:35 +01:00
* Also recalculate their level due to exp rewards
2023-03-03 15:23:46 +00:00
* @param pmcData Player profile
* @param body Completed quest request
* @param sessionID Session id
* @returns ItemEvent client response
*/
public completeQuest(
pmcData: IPmcData,
body: ICompleteQuestRequestData,
sessionID: string,
): IItemEventRouterResponse
2023-03-03 15:23:46 +00:00
{
const completeQuestResponse = this.eventOutputHolder.getOutput(sessionID);
const completedQuest = this.questHelper.getQuestFromDb(body.qid, pmcData);
const preCompleteProfileQuests = this.jsonUtil.clone(pmcData.Quests);
2023-03-03 15:23:46 +00:00
const completedQuestId = body.qid;
const beforeQuests = this.jsonUtil.clone(this.getClientQuests(sessionID)); // Must be gathered prior to applyQuestReward() & failQuests()
2023-03-03 15:23:46 +00:00
const newQuestState = QuestStatus.Success;
this.questHelper.updateQuestState(pmcData, newQuestState, completedQuestId);
const questRewards = this.questHelper.applyQuestReward(
pmcData,
body.qid,
newQuestState,
sessionID,
completeQuestResponse,
);
2023-03-03 15:23:46 +00:00
// Check for linked failed + unrestartable quests
2023-03-03 15:23:46 +00:00
const questsToFail = this.getQuestsFailedByCompletingQuest(completedQuestId);
if (questsToFail?.length > 0)
2023-03-03 15:23:46 +00:00
{
this.failQuests(sessionID, pmcData, questsToFail, completeQuestResponse);
2023-03-03 15:23:46 +00:00
}
// Show modal on player screen
this.sendSuccessDialogMessageOnQuestComplete(sessionID, pmcData, completedQuestId, questRewards);
// Add diff of quests before completion vs after for client response
2023-03-03 15:23:46 +00:00
const questDelta = this.questHelper.getDeltaQuests(beforeQuests, this.getClientQuests(sessionID));
// Check newly available + failed quests for timegates and add them to profile
this.addTimeLockedQuestsToProfile(pmcData, [...questDelta, ...questsToFail], body.qid);
2023-03-03 15:23:46 +00:00
// Inform client of quest changes
completeQuestResponse.profileChanges[sessionID].quests.push(...questDelta);
// Check if it's a repeatable quest. If so, remove from Quests and repeatable.activeQuests list + move to repeatable.inactiveQuests
2023-03-03 15:23:46 +00:00
for (const currentRepeatable of pmcData.RepeatableQuests)
{
const repeatableQuest = currentRepeatable.activeQuests.find((x) => x._id === completedQuestId);
2023-03-03 15:23:46 +00:00
if (repeatableQuest)
{
currentRepeatable.activeQuests = currentRepeatable.activeQuests.filter((x) =>
x._id !== completedQuestId
);
2023-03-03 15:23:46 +00:00
currentRepeatable.inactiveQuests.push(repeatableQuest);
// Need to remove redundant scav quest object as its no longer necessary, is tracked in pmc profile
this.removeQuestFromScavProfile(sessionID, repeatableQuest._id);
2023-03-03 15:23:46 +00:00
}
}
// Hydrate client response questsStatus array with data
const questStatusChanges = this.getQuestsWithDifferentStatuses(preCompleteProfileQuests, pmcData.Quests);
if (questStatusChanges)
{
completeQuestResponse.profileChanges[sessionID].questsStatus.push(...questStatusChanges);
}
2023-03-03 15:23:46 +00:00
// Recalculate level in event player leveled up
pmcData.Info.Level = this.playerService.calculateLevel(pmcData);
return completeQuestResponse;
}
/**
* Remove a quest entirely from a profile
* @param sessionId Player id
* @param questIdToRemove Qid of quest to remove
*/
protected removeQuestFromScavProfile(sessionId: string, questIdToRemove: string): void
{
const fullProfile = this.profileHelper.getFullProfile(sessionId);
const repeatableInScavProfile = fullProfile.characters.scav.Quests?.find((x) => x.qid === questIdToRemove);
if (!repeatableInScavProfile)
{
this.logger.warning(
`Unable to remove quest: ${questIdToRemove} from profile as scav quest cannot be found`,
);
return;
}
fullProfile.characters.scav.Quests.splice(
fullProfile.characters.scav.Quests.indexOf(repeatableInScavProfile),
1,
);
}
/**
* Return quests that have different statuses
* @param preQuestStatusus Quests before
* @param postQuestStatuses Quests after
* @returns QuestStatusChange array
*/
protected getQuestsWithDifferentStatuses(
preQuestStatusus: IQuestStatus[],
postQuestStatuses: IQuestStatus[],
): IQuestStatus[]
{
const result: IQuestStatus[] = [];
for (const quest of postQuestStatuses)
{
// Add quest if status differs or quest not found
const preQuest = preQuestStatusus.find((x) => x.qid === quest.qid);
if (!preQuest || preQuest.status !== quest.status)
{
result.push(quest);
}
}
if (result.length === 0)
{
return null;
}
return result;
}
2023-03-03 15:23:46 +00:00
/**
* Send a popup to player on successful completion of a quest
* @param sessionID session id
* @param pmcData Player profile
* @param completedQuestId Completed quest id
* @param questRewards Rewards given to player
*/
protected sendSuccessDialogMessageOnQuestComplete(
sessionID: string,
pmcData: IPmcData,
completedQuestId: string,
questRewards: Reward[],
): void
2023-03-03 15:23:46 +00:00
{
const quest = this.questHelper.getQuestFromDb(completedQuestId, pmcData);
this.mailSendService.sendLocalisedNpcMessageToPlayer(
sessionID,
this.traderHelper.getTraderById(quest.traderId),
MessageType.QUEST_SUCCESS,
quest.successMessageText,
questRewards,
this.timeUtil.getHoursAsSeconds(this.questConfig.redeemTime),
);
2023-03-03 15:23:46 +00:00
}
/**
* Look for newly available quests after completing a quest with a requirement to wait x minutes (time-locked) before being available and add data to profile
* @param pmcData Player profile to update
* @param quests Quests to look for wait conditions in
* @param completedQuestId Quest just completed
*/
protected addTimeLockedQuestsToProfile(pmcData: IPmcData, quests: IQuest[], completedQuestId: string): void
{
// Iterate over quests, look for quests with right criteria
for (const quest of quests)
{
// If quest has prereq of completed quest + availableAfter value > 0 (quest has wait time)
const nextQuestWaitCondition = quest.conditions.AvailableForStart.find((x) =>
x.target === completedQuestId && x.availableAfter > 0
);
2023-03-03 15:23:46 +00:00
if (nextQuestWaitCondition)
{
// Now + wait time
const availableAfterTimestamp = this.timeUtil.getTimestamp()
+ nextQuestWaitCondition.availableAfter;
2023-03-03 15:23:46 +00:00
// Update quest in profile with status of AvailableAfter
const existingQuestInProfile = pmcData.Quests.find((x) => x.qid === quest._id);
2023-03-03 15:23:46 +00:00
if (existingQuestInProfile)
{
existingQuestInProfile.availableAfter = availableAfterTimestamp;
existingQuestInProfile.status = QuestStatus.AvailableAfter;
2023-03-03 15:23:46 +00:00
existingQuestInProfile.startTime = 0;
existingQuestInProfile.statusTimers = {};
continue;
}
pmcData.Quests.push({
qid: quest._id,
startTime: 0,
status: QuestStatus.AvailableAfter,
statusTimers: {
// eslint-disable-next-line @typescript-eslint/naming-convention
"9": this.timeUtil.getTimestamp(),
},
availableAfter: availableAfterTimestamp,
2023-03-03 15:23:46 +00:00
});
}
}
}
/**
* Returns a list of quests that should be failed when a quest is completed
* @param completedQuestId quest completed id
* @returns array of quests
*/
protected getQuestsFailedByCompletingQuest(completedQuestId: string): IQuest[]
{
const questsInDb = this.questHelper.getQuestsFromDb();
return questsInDb.filter((x) =>
2023-03-03 15:23:46 +00:00
{
// No fail conditions, exit early
if (!x.conditions.Fail || x.conditions.Fail.length === 0)
{
return false;
}
return x.conditions.Fail.some((y) => y.target === completedQuestId);
2023-03-03 15:23:46 +00:00
});
}
/**
* Fail the provided quests
2023-03-03 15:23:46 +00:00
* Update quest in profile, otherwise add fresh quest object with failed status
* @param sessionID session id
* @param pmcData player profile
* @param questsToFail quests to fail
* @param output Client output
2023-03-03 15:23:46 +00:00
*/
protected failQuests(
sessionID: string,
pmcData: IPmcData,
questsToFail: IQuest[],
output: IItemEventRouterResponse,
): void
2023-03-03 15:23:46 +00:00
{
for (const questToFail of questsToFail)
{
// Skip failing a quest that has a fail status of something other than success
if (questToFail.conditions.Fail?.some((x) => x.status?.some((y) => y !== QuestStatus.Success)))
2023-03-03 15:23:46 +00:00
{
continue;
}
const isActiveQuestInPlayerProfile = pmcData.Quests.find((y) => y.qid === questToFail._id);
2023-03-03 15:23:46 +00:00
if (isActiveQuestInPlayerProfile)
{
const failBody: IFailQuestRequestData = {
Action: "QuestFail",
2023-03-03 15:23:46 +00:00
qid: questToFail._id,
removeExcessItems: true,
2023-03-03 15:23:46 +00:00
};
this.questHelper.failQuest(pmcData, failBody, sessionID, output);
2023-03-03 15:23:46 +00:00
}
else
{
const statusTimers = {};
statusTimers[QuestStatus.Fail] = this.timeUtil.getTimestamp();
const questData: IQuestStatus = {
2023-03-03 15:23:46 +00:00
qid: questToFail._id,
startTime: this.timeUtil.getTimestamp(),
statusTimers: statusTimers,
status: QuestStatus.Fail,
2023-03-03 15:23:46 +00:00
};
pmcData.Quests.push(questData);
}
}
}
/**
2023-07-15 11:00:35 +01:00
* Handle QuestHandover event
2023-03-03 15:23:46 +00:00
* @param pmcData Player profile
* @param handoverQuestRequest handover item request
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public handoverQuest(
pmcData: IPmcData,
handoverQuestRequest: IHandoverQuestRequestData,
sessionID: string,
): IItemEventRouterResponse
2023-03-03 15:23:46 +00:00
{
const quest = this.questHelper.getQuestFromDb(handoverQuestRequest.qid, pmcData);
const handoverQuestTypes = ["HandoverItem", "WeaponAssembly"];
const output = this.eventOutputHolder.getOutput(sessionID);
let isItemHandoverQuest = true;
2023-03-03 15:23:46 +00:00
let handedInCount = 0;
// Decrement number of items handed in
let handoverRequirements: AvailableForConditions;
for (const condition of quest.conditions.AvailableForFinish)
{
if (
condition.id === handoverQuestRequest.conditionId
&& handoverQuestTypes.includes(condition.conditionType)
)
2023-03-03 15:23:46 +00:00
{
handedInCount = Number.parseInt(<string>condition.value);
isItemHandoverQuest = condition.conditionType === handoverQuestTypes[0];
2023-03-03 15:23:46 +00:00
handoverRequirements = condition;
const profileCounter = (handoverQuestRequest.conditionId in pmcData.TaskConditionCounters)
? pmcData.TaskConditionCounters[handoverQuestRequest.conditionId].value
2023-03-03 15:23:46 +00:00
: 0;
handedInCount -= profileCounter;
if (handedInCount <= 0)
{
this.logger.error(
this.localisationService.getText(
"repeatable-quest_handover_failed_condition_already_satisfied",
{
questId: handoverQuestRequest.qid,
conditionId: handoverQuestRequest.conditionId,
profileCounter: profileCounter,
value: handedInCount,
},
),
);
2023-03-03 15:23:46 +00:00
return output;
}
break;
}
}
if (isItemHandoverQuest && handedInCount === 0)
2023-03-03 15:23:46 +00:00
{
return this.showRepeatableQuestInvalidConditionError(handoverQuestRequest, output);
2023-03-03 15:23:46 +00:00
}
2023-03-03 15:23:46 +00:00
let totalItemCountToRemove = 0;
for (const itemHandover of handoverQuestRequest.items)
{
const matchingItemInProfile = pmcData.Inventory.items.find((item) => item._id === itemHandover.id);
if (!matchingItemInProfile || !handoverRequirements.target.includes(matchingItemInProfile._tpl))
2023-03-03 15:23:46 +00:00
{
// Item handed in by player doesnt match what was requested
return this.showQuestItemHandoverMatchError(
handoverQuestRequest,
matchingItemInProfile,
handoverRequirements,
output,
);
2023-03-03 15:23:46 +00:00
}
// Remove the right quantity of given items
const itemCountToRemove = Math.min(itemHandover.count, handedInCount - totalItemCountToRemove);
totalItemCountToRemove += itemCountToRemove;
if (itemHandover.count - itemCountToRemove > 0)
{
// Remove single item with no children
this.questHelper.changeItemStack(
pmcData,
itemHandover.id,
itemHandover.count - itemCountToRemove,
sessionID,
output,
);
2023-03-03 15:23:46 +00:00
if (totalItemCountToRemove === handedInCount)
{
break;
}
}
else
{
// Remove item with children
const toRemove = this.itemHelper.findAndReturnChildrenByItems(pmcData.Inventory.items, itemHandover.id);
let index = pmcData.Inventory.items.length;
// Important: don't tell the client to remove the attachments, it will handle it
output.profileChanges[sessionID].items.del.push({ _id: itemHandover.id });
2023-03-03 15:23:46 +00:00
// Important: loop backward when removing items from the array we're looping on
while (index-- > 0)
{
if (toRemove.includes(pmcData.Inventory.items[index]._id))
{
pmcData.Inventory.items.splice(index, 1);
}
}
}
}
this.updateProfileTaskConditionCounterValue(
pmcData,
handoverQuestRequest.conditionId,
handoverQuestRequest.qid,
totalItemCountToRemove,
);
2023-03-03 15:23:46 +00:00
return output;
}
/**
* Show warning to user and write to log that repeatable quest failed a condition check
* @param handoverQuestRequest Quest request
* @param output Response to send to user
* @returns IItemEventRouterResponse
*/
protected showRepeatableQuestInvalidConditionError(
handoverQuestRequest: IHandoverQuestRequestData,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
const errorMessage = this.localisationService.getText("repeatable-quest_handover_failed_condition_invalid", {
questId: handoverQuestRequest.qid,
conditionId: handoverQuestRequest.conditionId,
});
this.logger.error(errorMessage);
return this.httpResponseUtil.appendErrorToOutput(output, errorMessage);
}
/**
* Show warning to user and write to log quest item handed over did not match what is required
* @param handoverQuestRequest Quest request
* @param itemHandedOver Non-matching item found
* @param handoverRequirements Quest handover requirements
* @param output Response to send to user
* @returns IItemEventRouterResponse
*/
protected showQuestItemHandoverMatchError(
handoverQuestRequest: IHandoverQuestRequestData,
itemHandedOver: Item,
handoverRequirements: AvailableForConditions,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{
const errorMessage = this.localisationService.getText("quest-handover_wrong_item", {
questId: handoverQuestRequest.qid,
handedInTpl: itemHandedOver._tpl,
requiredTpl: handoverRequirements.target[0],
});
this.logger.error(errorMessage);
return this.httpResponseUtil.appendErrorToOutput(output, errorMessage);
}
2023-03-03 15:23:46 +00:00
/**
* Increment a backend counter stored value by an amount,
* Create counter if it does not exist
* @param pmcData Profile to find backend counter in
* @param conditionId backend counter id to update
* @param questId quest id counter is associated with
* @param counterValue value to increment the backend counter with
*/
protected updateProfileTaskConditionCounterValue(
pmcData: IPmcData,
conditionId: string,
questId: string,
counterValue: number,
): void
2023-03-03 15:23:46 +00:00
{
if (pmcData.TaskConditionCounters[conditionId] !== undefined)
2023-03-03 15:23:46 +00:00
{
pmcData.TaskConditionCounters[conditionId].value += counterValue;
2023-03-03 15:23:46 +00:00
return;
}
pmcData.TaskConditionCounters[conditionId] = {
id: conditionId,
sourceId: questId,
type: "HandoverItem",
value: counterValue };
2023-03-03 15:23:46 +00:00
}
/**
* Handle /client/game/profile/items/moving - QuestFail
* @param pmcData Pmc profile
* @param request Fail qeust request
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
public failQuest(pmcData: IPmcData, request: IFailQuestRequestData, sessionID: string): IItemEventRouterResponse
{
return this.questHelper.failQuest(pmcData, request, sessionID);
}
}