2024-05-21 17:59:04 +00:00
|
|
|
import { DialogueHelper } from "@spt/helpers/DialogueHelper";
|
|
|
|
import { ItemHelper } from "@spt/helpers/ItemHelper";
|
|
|
|
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
|
|
|
import { QuestConditionHelper } from "@spt/helpers/QuestConditionHelper";
|
|
|
|
import { QuestHelper } from "@spt/helpers/QuestHelper";
|
|
|
|
import { TraderHelper } from "@spt/helpers/TraderHelper";
|
|
|
|
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
|
|
|
import { IQuestStatus } from "@spt/models/eft/common/tables/IBotBase";
|
2024-09-24 12:47:29 +01:00
|
|
|
import { IItem } from "@spt/models/eft/common/tables/IItem";
|
2024-05-21 17:59:04 +00:00
|
|
|
import { IQuest, IQuestCondition } from "@spt/models/eft/common/tables/IQuest";
|
|
|
|
import { IPmcDataRepeatableQuest, IRepeatableQuest } from "@spt/models/eft/common/tables/IRepeatableQuests";
|
|
|
|
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
|
|
|
|
import { IAcceptQuestRequestData } from "@spt/models/eft/quests/IAcceptQuestRequestData";
|
|
|
|
import { ICompleteQuestRequestData } from "@spt/models/eft/quests/ICompleteQuestRequestData";
|
|
|
|
import { IFailQuestRequestData } from "@spt/models/eft/quests/IFailQuestRequestData";
|
|
|
|
import { IHandoverQuestRequestData } from "@spt/models/eft/quests/IHandoverQuestRequestData";
|
|
|
|
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
|
|
|
|
import { MessageType } from "@spt/models/enums/MessageType";
|
|
|
|
import { QuestStatus } from "@spt/models/enums/QuestStatus";
|
|
|
|
import { IQuestConfig } from "@spt/models/spt/config/IQuestConfig";
|
|
|
|
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
|
|
|
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
|
|
|
|
import { ConfigServer } from "@spt/servers/ConfigServer";
|
2024-05-28 13:59:19 +01:00
|
|
|
import { DatabaseService } from "@spt/services/DatabaseService";
|
2024-05-21 17:59:04 +00:00
|
|
|
import { LocaleService } from "@spt/services/LocaleService";
|
|
|
|
import { LocalisationService } from "@spt/services/LocalisationService";
|
|
|
|
import { MailSendService } from "@spt/services/MailSendService";
|
|
|
|
import { PlayerService } from "@spt/services/PlayerService";
|
|
|
|
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
|
|
|
|
import { TimeUtil } from "@spt/utils/TimeUtil";
|
2024-07-23 11:12:53 -04:00
|
|
|
import { ICloner } from "@spt/utils/cloners/ICloner";
|
|
|
|
import { inject, injectable } from "tsyringe";
|
2023-03-03 15:23:46 +00:00
|
|
|
|
|
|
|
@injectable()
|
2024-07-23 11:12:53 -04:00
|
|
|
export class QuestController {
|
2023-03-03 15:23:46 +00:00
|
|
|
protected questConfig: IQuestConfig;
|
|
|
|
|
|
|
|
constructor(
|
2024-05-28 14:04:20 +00:00
|
|
|
@inject("PrimaryLogger") protected logger: ILogger,
|
2023-03-03 15:23:46 +00:00
|
|
|
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
|
|
|
@inject("HttpResponseUtil") protected httpResponseUtil: HttpResponseUtil,
|
|
|
|
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
|
2024-05-28 13:59:19 +01:00
|
|
|
@inject("DatabaseService") protected databaseService: DatabaseService,
|
2023-03-03 15:23:46 +00:00
|
|
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
|
|
|
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
|
2023-07-21 17:08:32 +00:00
|
|
|
@inject("MailSendService") protected mailSendService: MailSendService,
|
2023-03-03 15:23:46 +00:00
|
|
|
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
2023-07-21 17:08:32 +00:00
|
|
|
@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("LocalisationService") protected localisationService: LocalisationService,
|
2023-11-16 21:42:06 +00:00
|
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
2024-05-28 14:04:20 +00:00
|
|
|
@inject("PrimaryCloner") protected cloner: ICloner,
|
2024-07-23 11:12:53 -04:00
|
|
|
) {
|
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
|
|
|
|
*/
|
2024-07-23 11:12:53 -04:00
|
|
|
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);
|
|
|
|
|
2024-07-23 11:12:53 -04:00
|
|
|
for (const quest of allQuests) {
|
2023-07-09 14:45:06 +01:00
|
|
|
// Player already accepted the quest, show it regardless of status
|
2024-05-17 15:32:41 -04:00
|
|
|
const questInProfile = profile.Quests.find((x) => x.qid === quest._id);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (questInProfile) {
|
2023-10-10 11:03:20 +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;
|
|
|
|
}
|
|
|
|
|
2023-07-09 14:45:06 +01:00
|
|
|
// Filter out bear quests for usec and vice versa
|
2024-07-23 11:12:53 -04:00
|
|
|
if (this.questHelper.questIsForOtherSide(profile.Info.Side, quest._id)) {
|
2023-03-03 15:23:46 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!this.questHelper.showEventQuestToPlayer(quest._id)) {
|
2023-07-09 14:45:06 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
// Don't add quests that have a level higher than the user's
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!this.playerLevelFulfillsQuestRequirement(quest, profile.Info.Level)) {
|
2023-07-10 15:48:49 +01:00
|
|
|
continue;
|
2023-12-26 16:26:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Player can use trader mods then remove them, leaving quests behind
|
|
|
|
const trader = profile.TradersInfo[quest.traderId];
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!trader) {
|
2024-02-02 13:54:07 -05:00
|
|
|
this.logger.debug(
|
|
|
|
`Unable to show quest: ${quest.QuestName} as its for a trader: ${quest.traderId} that no longer exists.`,
|
|
|
|
);
|
2023-12-26 16:26:15 +00:00
|
|
|
|
|
|
|
continue;
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const questRequirements = this.questConditionHelper.getQuestConditions(quest.conditions.AvailableForStart);
|
2023-11-16 21:42:06 +00:00
|
|
|
const loyaltyRequirements = this.questConditionHelper.getLoyaltyConditions(
|
|
|
|
quest.conditions.AvailableForStart,
|
|
|
|
);
|
|
|
|
const standingRequirements = this.questConditionHelper.getStandingConditions(
|
|
|
|
quest.conditions.AvailableForStart,
|
|
|
|
);
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2023-10-10 11:03:20 +00:00
|
|
|
// Quest has no conditions, standing or loyalty conditions, add to visible quest list
|
2024-05-17 15:32:41 -04:00
|
|
|
if (
|
2024-07-23 11:12:53 -04:00
|
|
|
questRequirements.length === 0 &&
|
|
|
|
loyaltyRequirements.length === 0 &&
|
|
|
|
standingRequirements.length === 0
|
|
|
|
) {
|
2023-10-10 11:03:20 +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;
|
2024-07-23 11:12:53 -04:00
|
|
|
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
|
2024-05-17 15:32:41 -04:00
|
|
|
const prerequisiteQuest = profile.Quests.find((profileQuest) =>
|
2024-05-07 23:57:08 -04:00
|
|
|
conditionToFulfil.target.includes(profileQuest.qid),
|
2024-02-02 13:54:07 -05:00
|
|
|
);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!prerequisiteQuest) {
|
2023-03-03 15:23:46 +00:00
|
|
|
haveCompletedPreviousQuest = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-10-10 11:03:20 +00:00
|
|
|
// Prereq does not have its status requirement fulfilled
|
2024-02-17 10:13:19 +00:00
|
|
|
// Some bsg status ids are strings, MUST convert to number before doing includes check
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!conditionToFulfil.status.map((status) => Number(status)).includes(prerequisiteQuest.status)) {
|
2023-10-10 11:03:20 +00:00
|
|
|
haveCompletedPreviousQuest = false;
|
|
|
|
break;
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
2023-10-10 14:46:23 +01:00
|
|
|
|
|
|
|
// Has a wait timer
|
2024-07-23 11:12:53 -04:00
|
|
|
if (conditionToFulfil.availableAfter > 0) {
|
2023-10-10 14:46:23 +01:00
|
|
|
// Compare current time to unlock time for previous quest
|
|
|
|
const previousQuestCompleteTime = prerequisiteQuest.statusTimers[prerequisiteQuest.status];
|
2023-12-27 17:15:38 +00:00
|
|
|
const unlockTime = previousQuestCompleteTime + conditionToFulfil.availableAfter;
|
2024-07-23 11:12:53 -04:00
|
|
|
if (unlockTime > this.timeUtil.getTimestamp()) {
|
2023-11-16 21:42:06 +00:00
|
|
|
this.logger.debug(
|
|
|
|
`Quest ${quest.QuestName} is locked for another ${
|
|
|
|
unlockTime - this.timeUtil.getTimestamp()
|
|
|
|
} seconds`,
|
|
|
|
);
|
2023-10-10 14:46:23 +01:00
|
|
|
}
|
|
|
|
}
|
2023-10-10 11:03:20 +00:00
|
|
|
}
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2023-10-10 11:03:20 +00:00
|
|
|
// Previous quest not completed, skip
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!haveCompletedPreviousQuest) {
|
2023-10-10 11:03:20 +00:00
|
|
|
continue;
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let passesLoyaltyRequirements = true;
|
2024-07-23 11:12:53 -04:00
|
|
|
for (const condition of loyaltyRequirements) {
|
|
|
|
if (!this.questHelper.traderLoyaltyLevelRequirementCheck(condition, profile)) {
|
2023-03-03 15:23:46 +00:00
|
|
|
passesLoyaltyRequirements = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-10 11:03:20 +00:00
|
|
|
let passesStandingRequirements = true;
|
2024-07-23 11:12:53 -04:00
|
|
|
for (const condition of standingRequirements) {
|
|
|
|
if (!this.questHelper.traderStandingRequirementCheck(condition, profile)) {
|
2023-10-10 11:03:20 +00:00
|
|
|
passesStandingRequirements = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-23 11:12:53 -04:00
|
|
|
if (haveCompletedPreviousQuest && passesLoyaltyRequirements && passesStandingRequirements) {
|
2023-10-10 11:03:20 +00:00
|
|
|
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
|
|
|
|
*/
|
2024-07-23 11:12:53 -04:00
|
|
|
protected playerLevelFulfillsQuestRequirement(quest: IQuest, playerLevel: number): boolean {
|
|
|
|
if (!quest.conditions) {
|
2024-07-23 09:07:07 +01:00
|
|
|
// No conditions
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-07-10 15:48:49 +01:00
|
|
|
const levelConditions = this.questConditionHelper.getLevelConditions(quest.conditions.AvailableForStart);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (levelConditions.length) {
|
|
|
|
for (const levelCondition of levelConditions) {
|
|
|
|
if (!this.questHelper.doesPlayerLevelFulfilCondition(playerLevel, levelCondition)) {
|
2023-07-10 15:48:49 +01:00
|
|
|
// 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
|
2023-11-16 21:42:06 +00:00
|
|
|
return true;
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
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
|
2023-11-29 23:36:31 +00:00
|
|
|
* @returns Client response
|
2023-03-03 15:23:46 +00:00
|
|
|
*/
|
2023-11-16 21:42:06 +00:00
|
|
|
public acceptQuest(
|
|
|
|
pmcData: IPmcData,
|
|
|
|
acceptedQuest: IAcceptQuestRequestData,
|
|
|
|
sessionID: string,
|
2024-07-23 11:12:53 -04:00
|
|
|
): IItemEventRouterResponse {
|
2023-03-03 15:23:46 +00:00
|
|
|
const acceptQuestResponse = this.eventOutputHolder.getOutput(sessionID);
|
2024-02-02 13:54:07 -05:00
|
|
|
|
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
|
2024-05-17 15:32:41 -04:00
|
|
|
const existingQuestStatus = pmcData.Quests.find((x) => x.qid === acceptedQuest.qid);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (existingQuestStatus) {
|
2023-03-03 15:23:46 +00:00
|
|
|
// Update existing
|
2023-11-29 23:36:31 +00:00
|
|
|
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);
|
2024-07-23 11:12:53 -04:00
|
|
|
} else {
|
2023-03-03 15:23:46 +00:00
|
|
|
// Add new quest to server profile
|
2023-11-29 23:36:31 +00:00
|
|
|
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-11-29 23:36:31 +00:00
|
|
|
|
2024-09-22 10:46:50 +01:00
|
|
|
this.addTaskConditionCountersToProfile(questFromDb.conditions.AvailableForFinish, pmcData, acceptedQuest.qid);
|
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
// Get messageId of text to send to player as text message in game
|
2023-11-16 21:42:06 +00:00
|
|
|
const messageId = this.questHelper.getMessageIdForQuestStart(
|
|
|
|
questFromDb.startedMessageText,
|
|
|
|
questFromDb.description,
|
|
|
|
);
|
2023-11-29 23:36:31 +00:00
|
|
|
|
2023-11-30 09:36:28 +00:00
|
|
|
// Apply non-item rewards to profile + return item rewards
|
|
|
|
const startedQuestRewardItems = this.questHelper.applyQuestReward(
|
2023-11-16 21:42:06 +00:00
|
|
|
pmcData,
|
|
|
|
acceptedQuest.qid,
|
|
|
|
QuestStatus.Started,
|
|
|
|
sessionID,
|
|
|
|
acceptQuestResponse,
|
|
|
|
);
|
2023-07-22 12:56:15 +01:00
|
|
|
|
2023-11-30 09:36:28 +00:00
|
|
|
// Send started text + any starting reward items found above to player
|
2023-07-22 12:56:15 +01:00
|
|
|
this.mailSendService.sendLocalisedNpcMessageToPlayer(
|
|
|
|
sessionID,
|
|
|
|
this.traderHelper.getTraderById(questFromDb.traderId),
|
|
|
|
MessageType.QUEST_START,
|
|
|
|
messageId,
|
2023-11-30 09:36:28 +00:00
|
|
|
startedQuestRewardItems,
|
2024-05-20 11:31:45 +01:00
|
|
|
this.timeUtil.getHoursAsSeconds(this.questHelper.getMailItemRedeemTimeHoursForProfile(pmcData)),
|
2023-11-16 21:42:06 +00:00
|
|
|
);
|
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
|
2024-02-02 13:54:07 -05:00
|
|
|
acceptQuestResponse.profileChanges[sessionID].quests.push(
|
|
|
|
...this.questHelper.getNewlyAccessibleQuestsWhenStartingQuest(acceptedQuest.qid, sessionID),
|
|
|
|
);
|
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
return acceptQuestResponse;
|
|
|
|
}
|
|
|
|
|
2024-09-22 10:46:50 +01:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param questConditions Conditions to iterate over and possibly add to profile
|
|
|
|
* @param pmcData Profile to add to
|
|
|
|
* @param questId Quest conditions came from
|
|
|
|
*/
|
|
|
|
protected addTaskConditionCountersToProfile(
|
|
|
|
questConditions: IQuestCondition[],
|
|
|
|
pmcData: IPmcData,
|
|
|
|
questId: string,
|
|
|
|
) {
|
|
|
|
for (const condition of questConditions) {
|
|
|
|
if (pmcData.TaskConditionCounters[condition.id]) {
|
|
|
|
this.logger.error(
|
|
|
|
`Unable to add new task condition counter: ${condition.conditionType} for qeust: ${questId} to profile: ${pmcData.sessionId} as it already exists:`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (condition.conditionType) {
|
|
|
|
case "SellItemToTrader":
|
|
|
|
pmcData.TaskConditionCounters[condition.id] = {
|
|
|
|
id: condition.id,
|
|
|
|
sourceId: questId,
|
|
|
|
type: condition.conditionType,
|
|
|
|
value: 0,
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2023-11-16 21:42:06 +00:00
|
|
|
public acceptRepeatableQuest(
|
|
|
|
pmcData: IPmcData,
|
|
|
|
acceptedQuest: IAcceptQuestRequestData,
|
|
|
|
sessionID: string,
|
2024-07-23 11:12:53 -04:00
|
|
|
): IItemEventRouterResponse {
|
2023-11-27 16:35:44 +00:00
|
|
|
// Create and store quest status object inside player profile
|
2024-02-02 13:54:07 -05:00
|
|
|
const newRepeatableQuest = this.questHelper.getQuestReadyForProfile(
|
|
|
|
pmcData,
|
|
|
|
QuestStatus.Started,
|
|
|
|
acceptedQuest,
|
|
|
|
);
|
2023-11-27 16:35:44 +00:00
|
|
|
pmcData.Quests.push(newRepeatableQuest);
|
2023-11-16 21:42:06 +00:00
|
|
|
|
2023-11-27 16:35:44 +00:00
|
|
|
// Look for the generated quest cache in profile.RepeatableQuests
|
2023-03-03 15:23:46 +00:00
|
|
|
const repeatableQuestProfile = this.getRepeatableQuestFromProfile(pmcData, acceptedQuest);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!repeatableQuestProfile) {
|
2023-11-16 21:42:06 +00:00
|
|
|
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"));
|
|
|
|
}
|
|
|
|
|
2023-10-17 22:04:19 +01:00
|
|
|
// Some scav quests need to be added to scav profile for them to show up in-raid
|
2023-11-16 21:42:06 +00:00
|
|
|
if (
|
2024-07-23 11:12:53 -04:00
|
|
|
repeatableQuestProfile.side === "Scav" &&
|
|
|
|
["PickUp", "Exploration", "Elimination"].includes(repeatableQuestProfile.type)
|
|
|
|
) {
|
2023-10-17 16:31:21 +01:00
|
|
|
const fullProfile = this.profileHelper.getFullProfile(sessionID);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!fullProfile.characters.scav.Quests) {
|
2023-10-17 16:31:21 +01:00
|
|
|
fullProfile.characters.scav.Quests = [];
|
|
|
|
}
|
|
|
|
|
2023-11-27 16:35:44 +00:00
|
|
|
fullProfile.characters.scav.Quests.push(newRepeatableQuest);
|
2023-10-17 16:31:21 +01:00
|
|
|
}
|
|
|
|
|
2024-06-03 16:20:16 +01:00
|
|
|
const response = this.createAcceptedQuestClientResponse(sessionID, pmcData, repeatableQuestProfile);
|
|
|
|
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected createAcceptedQuestClientResponse(
|
|
|
|
sessionID: string,
|
|
|
|
pmcData: IPmcData,
|
2024-07-23 11:12:53 -04:00
|
|
|
repeatableQuestProfile: IRepeatableQuest,
|
|
|
|
): IItemEventRouterResponse {
|
|
|
|
const repeatableSettings = pmcData.RepeatableQuests.find(
|
|
|
|
(quest) => quest.name === repeatableQuestProfile.sptRepatableGroupName,
|
|
|
|
);
|
2023-11-27 16:35:44 +00:00
|
|
|
|
2023-10-19 21:36:17 +01:00
|
|
|
const change = {};
|
2024-07-23 17:30:20 +01:00
|
|
|
change[repeatableQuestProfile._id] = repeatableSettings.changeRequirement[repeatableQuestProfile._id];
|
2024-06-03 16:20:16 +01:00
|
|
|
|
|
|
|
const repeatableData: IPmcDataRepeatableQuest = {
|
2024-05-17 15:32:41 -04:00
|
|
|
id:
|
2024-07-23 11:12:53 -04:00
|
|
|
repeatableSettings.id ??
|
|
|
|
this.questConfig.repeatableQuests.find(
|
|
|
|
(repeatableQuest) => repeatableQuest.name === repeatableQuestProfile.sptRepatableGroupName,
|
|
|
|
).id,
|
2023-10-19 20:43:42 +01:00
|
|
|
name: repeatableSettings.name,
|
|
|
|
endTime: repeatableSettings.endTime,
|
2023-10-19 21:36:17 +01:00
|
|
|
changeRequirement: change,
|
2023-10-19 21:06:09 +01:00
|
|
|
activeQuests: [repeatableQuestProfile],
|
2023-11-16 21:42:06 +00:00
|
|
|
inactiveQuests: [],
|
2024-05-30 13:05:28 +01:00
|
|
|
freeChanges: repeatableSettings.freeChanges,
|
|
|
|
freeChangesAvailable: repeatableSettings.freeChangesAvailable,
|
2023-10-19 20:43:42 +01:00
|
|
|
};
|
2023-11-30 10:02:00 +00:00
|
|
|
|
2024-06-03 16:20:16 +01:00
|
|
|
// Nullguard
|
|
|
|
const acceptQuestResponse = this.eventOutputHolder.getOutput(sessionID);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!acceptQuestResponse.profileChanges[sessionID].repeatableQuests) {
|
2024-02-02 13:54:07 -05:00
|
|
|
acceptQuestResponse.profileChanges[sessionID].repeatableQuests = [];
|
2023-11-30 10:02:00 +00:00
|
|
|
}
|
2024-06-03 16:20:16 +01:00
|
|
|
|
|
|
|
// Add constructed objet into response
|
|
|
|
acceptQuestResponse.profileChanges[sessionID].repeatableQuests.push(repeatableData);
|
2023-10-17 16:31:21 +01:00
|
|
|
|
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
|
|
|
|
*/
|
2024-05-17 15:32:41 -04:00
|
|
|
protected getRepeatableQuestFromProfile(
|
|
|
|
pmcData: IPmcData,
|
|
|
|
acceptedQuest: IAcceptQuestRequestData,
|
2024-07-23 11:12:53 -04:00
|
|
|
): IRepeatableQuest {
|
|
|
|
for (const repeatableQuest of pmcData.RepeatableQuests) {
|
2024-05-17 15:32:41 -04:00
|
|
|
const matchingQuest = repeatableQuest.activeQuests.find((x) => x._id === acceptedQuest.qid);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (matchingQuest) {
|
2023-03-21 14:19:49 +00:00
|
|
|
this.logger.debug(`Accepted repeatable quest ${acceptedQuest.qid} from ${repeatableQuest.name}`);
|
2023-10-19 20:43:42 +01:00
|
|
|
matchingQuest.sptRepatableGroupName = repeatableQuest.name;
|
2023-11-16 21:42:06 +00:00
|
|
|
|
2023-03-21 14:22:45 +00:00
|
|
|
return matchingQuest;
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-21 14:19:49 +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
|
|
|
|
*/
|
2023-11-16 21:42:06 +00:00
|
|
|
public completeQuest(
|
|
|
|
pmcData: IPmcData,
|
|
|
|
body: ICompleteQuestRequestData,
|
|
|
|
sessionID: string,
|
2024-07-23 11:12:53 -04:00
|
|
|
): IItemEventRouterResponse {
|
2023-03-03 15:23:46 +00:00
|
|
|
const completeQuestResponse = this.eventOutputHolder.getOutput(sessionID);
|
|
|
|
|
2023-10-10 11:03:20 +00:00
|
|
|
const completedQuest = this.questHelper.getQuestFromDb(body.qid, pmcData);
|
2024-05-13 17:58:17 +00:00
|
|
|
const preCompleteProfileQuests = this.cloner.clone(pmcData.Quests);
|
2023-10-10 11:03:20 +00:00
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
const completedQuestId = body.qid;
|
2024-05-13 17:58:17 +00:00
|
|
|
const clientQuestsClone = this.cloner.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);
|
2023-11-16 21:42:06 +00:00
|
|
|
const questRewards = this.questHelper.applyQuestReward(
|
|
|
|
pmcData,
|
|
|
|
body.qid,
|
|
|
|
newQuestState,
|
|
|
|
sessionID,
|
|
|
|
completeQuestResponse,
|
|
|
|
);
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2024-02-17 10:46:38 +00:00
|
|
|
// Check for linked failed + unrestartable quests (only get quests not already failed
|
|
|
|
const questsToFail = this.getQuestsFailedByCompletingQuest(completedQuestId, pmcData);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (questsToFail?.length > 0) {
|
2024-02-17 10:46:38 +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);
|
|
|
|
|
2023-10-10 11:03:20 +00:00
|
|
|
// Add diff of quests before completion vs after for client response
|
2024-02-05 14:43:46 +00:00
|
|
|
const questDelta = this.questHelper.getDeltaQuests(clientQuestsClone, this.getClientQuests(sessionID));
|
2023-11-16 21:42:06 +00:00
|
|
|
|
2023-10-10 16:18:55 +01:00
|
|
|
// Check newly available + failed quests for timegates and add them to profile
|
2024-01-14 12:29:58 +00:00
|
|
|
this.addTimeLockedQuestsToProfile(pmcData, [...questDelta], body.qid);
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2023-10-10 16:18:55 +01:00
|
|
|
// Inform client of quest changes
|
2023-11-30 10:02:00 +00:00
|
|
|
completeQuestResponse.profileChanges[sessionID].quests.push(...questDelta);
|
2023-10-10 16:18:55 +01:00
|
|
|
|
2024-01-31 14:38:18 +00:00
|
|
|
// Check if it's a repeatable quest. If so, remove from Quests
|
2024-07-23 11:12:53 -04:00
|
|
|
for (const currentRepeatable of pmcData.RepeatableQuests) {
|
2024-05-17 15:32:41 -04:00
|
|
|
const repeatableQuest = currentRepeatable.activeQuests.find(
|
|
|
|
(activeRepeatable) => activeRepeatable._id === completedQuestId,
|
2024-02-02 13:54:07 -05:00
|
|
|
);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (repeatableQuest) {
|
2023-10-17 22:04:19 +01:00
|
|
|
// Need to remove redundant scav quest object as its no longer necessary, is tracked in pmc profile
|
2024-07-23 11:12:53 -04:00
|
|
|
if (repeatableQuest.side === "Scav") {
|
2024-01-05 22:55:19 +00:00
|
|
|
this.removeQuestFromScavProfile(sessionID, repeatableQuest._id);
|
|
|
|
}
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-10 11:03:20 +00:00
|
|
|
// Hydrate client response questsStatus array with data
|
|
|
|
const questStatusChanges = this.getQuestsWithDifferentStatuses(preCompleteProfileQuests, pmcData.Quests);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (questStatusChanges) {
|
2023-10-10 11:03:20 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-02-17 10:46:38 +00:00
|
|
|
/**
|
|
|
|
* Return a list of quests that would fail when supplied quest is completed
|
|
|
|
* @param completedQuestId quest completed id
|
|
|
|
* @returns array of IQuest objects
|
|
|
|
*/
|
2024-07-23 11:12:53 -04:00
|
|
|
protected getQuestsFailedByCompletingQuest(completedQuestId: string, pmcProfile: IPmcData): IQuest[] {
|
2024-02-17 10:46:38 +00:00
|
|
|
const questsInDb = this.questHelper.getQuestsFromDb();
|
2024-07-23 11:12:53 -04:00
|
|
|
return questsInDb.filter((quest) => {
|
2024-02-17 10:46:38 +00:00
|
|
|
// No fail conditions, skip
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!quest.conditions.Fail || quest.conditions.Fail.length === 0) {
|
2024-02-17 10:46:38 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Quest already failed in profile, skip
|
|
|
|
if (
|
2024-05-17 15:32:41 -04:00
|
|
|
pmcProfile.Quests.some(
|
|
|
|
(profileQuest) => profileQuest.qid === quest._id && profileQuest.status === QuestStatus.Fail,
|
2024-02-17 10:46:38 +00:00
|
|
|
)
|
2024-07-23 11:12:53 -04:00
|
|
|
) {
|
2024-02-17 10:46:38 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-05-17 15:32:41 -04:00
|
|
|
return quest.conditions.Fail.some((condition) => condition.target?.includes(completedQuestId));
|
2024-02-17 10:46:38 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-10-17 22:04:19 +01:00
|
|
|
/**
|
|
|
|
* Remove a quest entirely from a profile
|
|
|
|
* @param sessionId Player id
|
|
|
|
* @param questIdToRemove Qid of quest to remove
|
|
|
|
*/
|
2024-07-23 11:12:53 -04:00
|
|
|
protected removeQuestFromScavProfile(sessionId: string, questIdToRemove: string): void {
|
2023-10-17 22:04:19 +01:00
|
|
|
const fullProfile = this.profileHelper.getFullProfile(sessionId);
|
2024-05-17 15:32:41 -04:00
|
|
|
const repeatableInScavProfile = fullProfile.characters.scav.Quests?.find((x) => x.qid === questIdToRemove);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!repeatableInScavProfile) {
|
|
|
|
this.logger.warning(
|
|
|
|
this.localisationService.getText("quest-unable_to_remove_scav_quest_from_profile", {
|
2024-05-24 16:42:42 +01:00
|
|
|
scavQuestId: questIdToRemove,
|
|
|
|
profileId: sessionId,
|
2024-07-23 11:12:53 -04:00
|
|
|
}),
|
|
|
|
);
|
2023-10-17 22:04:19 +01:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-11-16 21:42:06 +00:00
|
|
|
fullProfile.characters.scav.Quests.splice(
|
|
|
|
fullProfile.characters.scav.Quests.indexOf(repeatableInScavProfile),
|
|
|
|
1,
|
|
|
|
);
|
2023-10-17 22:04:19 +01:00
|
|
|
}
|
|
|
|
|
2023-10-10 11:03:20 +00:00
|
|
|
/**
|
|
|
|
* Return quests that have different statuses
|
|
|
|
* @param preQuestStatusus Quests before
|
|
|
|
* @param postQuestStatuses Quests after
|
|
|
|
* @returns QuestStatusChange array
|
|
|
|
*/
|
2023-11-16 21:42:06 +00:00
|
|
|
protected getQuestsWithDifferentStatuses(
|
|
|
|
preQuestStatusus: IQuestStatus[],
|
|
|
|
postQuestStatuses: IQuestStatus[],
|
2024-07-23 11:12:53 -04:00
|
|
|
): IQuestStatus[] | undefined {
|
2023-10-10 11:03:20 +00:00
|
|
|
const result: IQuestStatus[] = [];
|
|
|
|
|
2024-07-23 11:12:53 -04:00
|
|
|
for (const quest of postQuestStatuses) {
|
2023-10-10 11:03:20 +00:00
|
|
|
// Add quest if status differs or quest not found
|
2024-05-17 15:32:41 -04:00
|
|
|
const preQuest = preQuestStatusus.find((x) => x.qid === quest.qid);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (!preQuest || preQuest.status !== quest.status) {
|
2023-10-10 11:03:20 +00:00
|
|
|
result.push(quest);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-23 11:12:53 -04:00
|
|
|
if (result.length === 0) {
|
2024-05-27 20:06:07 +00:00
|
|
|
return undefined;
|
2023-10-10 11:03:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
*/
|
2023-11-16 21:42:06 +00:00
|
|
|
protected sendSuccessDialogMessageOnQuestComplete(
|
|
|
|
sessionID: string,
|
|
|
|
pmcData: IPmcData,
|
|
|
|
completedQuestId: string,
|
2024-09-24 12:47:29 +01:00
|
|
|
questRewards: IItem[],
|
2024-07-23 11:12:53 -04:00
|
|
|
): void {
|
2023-03-03 15:23:46 +00:00
|
|
|
const quest = this.questHelper.getQuestFromDb(completedQuestId, pmcData);
|
|
|
|
|
2023-07-21 17:08:32 +00:00
|
|
|
this.mailSendService.sendLocalisedNpcMessageToPlayer(
|
|
|
|
sessionID,
|
|
|
|
this.traderHelper.getTraderById(quest.traderId),
|
|
|
|
MessageType.QUEST_SUCCESS,
|
|
|
|
quest.successMessageText,
|
|
|
|
questRewards,
|
2024-05-20 11:31:45 +01:00
|
|
|
this.timeUtil.getHoursAsSeconds(this.questHelper.getMailItemRedeemTimeHoursForProfile(pmcData)),
|
2023-11-16 21:42:06 +00:00
|
|
|
);
|
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
|
|
|
|
*/
|
2024-07-23 11:12:53 -04:00
|
|
|
protected addTimeLockedQuestsToProfile(pmcData: IPmcData, quests: IQuest[], completedQuestId: string): void {
|
2023-03-03 15:23:46 +00:00
|
|
|
// Iterate over quests, look for quests with right criteria
|
2024-07-23 11:12:53 -04:00
|
|
|
for (const quest of quests) {
|
2023-10-10 11:03:20 +00:00
|
|
|
// If quest has prereq of completed quest + availableAfter value > 0 (quest has wait time)
|
2024-05-17 15:32:41 -04:00
|
|
|
const nextQuestWaitCondition = quest.conditions.AvailableForStart.find(
|
|
|
|
(x) => x.target?.includes(completedQuestId) && x.availableAfter > 0,
|
2023-11-16 21:42:06 +00:00
|
|
|
);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (nextQuestWaitCondition) {
|
2023-10-10 11:03:20 +00:00
|
|
|
// Now + wait time
|
2024-02-02 13:54:07 -05:00
|
|
|
const availableAfterTimestamp = this.timeUtil.getTimestamp() + nextQuestWaitCondition.availableAfter;
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2023-10-10 11:03:20 +00:00
|
|
|
// Update quest in profile with status of AvailableAfter
|
2024-05-17 15:32:41 -04:00
|
|
|
const existingQuestInProfile = pmcData.Quests.find((x) => x.qid === quest._id);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (existingQuestInProfile) {
|
2023-03-03 15:23:46 +00:00
|
|
|
existingQuestInProfile.availableAfter = availableAfterTimestamp;
|
2023-10-10 11:03:20 +00:00
|
|
|
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,
|
2023-10-10 11:03:20 +00:00
|
|
|
status: QuestStatus.AvailableAfter,
|
2023-10-10 16:18:55 +01:00
|
|
|
statusTimers: {
|
2024-05-07 23:57:08 -04:00
|
|
|
9: this.timeUtil.getTimestamp(),
|
2023-10-10 16:18:55 +01:00
|
|
|
},
|
2023-11-16 21:42:06 +00:00
|
|
|
availableAfter: availableAfterTimestamp,
|
2023-03-03 15:23:46 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-07-25 19:20:17 +01: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
|
2023-10-10 11:03:20 +00:00
|
|
|
* @param output Client output
|
2023-03-03 15:23:46 +00:00
|
|
|
*/
|
2023-11-16 21:42:06 +00:00
|
|
|
protected failQuests(
|
|
|
|
sessionID: string,
|
|
|
|
pmcData: IPmcData,
|
|
|
|
questsToFail: IQuest[],
|
|
|
|
output: IItemEventRouterResponse,
|
2024-07-23 11:12:53 -04:00
|
|
|
): void {
|
|
|
|
for (const questToFail of questsToFail) {
|
2023-07-25 19:20:17 +01:00
|
|
|
// Skip failing a quest that has a fail status of something other than success
|
2024-07-23 11:12:53 -04:00
|
|
|
if (questToFail.conditions.Fail?.some((x) => x.status?.some((status) => status !== QuestStatus.Success))) {
|
2023-03-03 15:23:46 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-05-17 15:32:41 -04:00
|
|
|
const isActiveQuestInPlayerProfile = pmcData.Quests.find((quest) => quest.qid === questToFail._id);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (isActiveQuestInPlayerProfile) {
|
|
|
|
if (isActiveQuestInPlayerProfile.status !== QuestStatus.Fail) {
|
2024-01-14 12:29:58 +00:00
|
|
|
const failBody: IFailQuestRequestData = {
|
|
|
|
Action: "QuestFail",
|
|
|
|
qid: questToFail._id,
|
|
|
|
removeExcessItems: true,
|
|
|
|
};
|
|
|
|
this.questHelper.failQuest(pmcData, failBody, sessionID, output);
|
|
|
|
}
|
2024-07-23 11:12:53 -04:00
|
|
|
} else {
|
2024-01-14 12:29:58 +00:00
|
|
|
// Failing an entirely new quest that doesnt exist in profile
|
2023-10-10 11:03:20 +00:00
|
|
|
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(),
|
2023-10-10 11:03:20 +00:00
|
|
|
statusTimers: statusTimers,
|
2023-11-16 21:42:06 +00:00
|
|
|
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
|
|
|
|
*/
|
2023-11-16 21:42:06 +00:00
|
|
|
public handoverQuest(
|
|
|
|
pmcData: IPmcData,
|
|
|
|
handoverQuestRequest: IHandoverQuestRequestData,
|
|
|
|
sessionID: string,
|
2024-07-23 11:12:53 -04:00
|
|
|
): 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);
|
2023-03-13 09:34:04 +00:00
|
|
|
|
|
|
|
let isItemHandoverQuest = true;
|
2023-03-03 15:23:46 +00:00
|
|
|
let handedInCount = 0;
|
|
|
|
|
|
|
|
// Decrement number of items handed in
|
2024-01-05 19:52:21 +00:00
|
|
|
let handoverRequirements: IQuestCondition;
|
2024-07-23 11:12:53 -04:00
|
|
|
for (const condition of quest.conditions.AvailableForFinish) {
|
2023-11-16 21:42:06 +00:00
|
|
|
if (
|
2024-07-23 11:12:53 -04:00
|
|
|
condition.id === handoverQuestRequest.conditionId &&
|
|
|
|
handoverQuestTypes.includes(condition.conditionType)
|
|
|
|
) {
|
2023-12-27 17:15:38 +00:00
|
|
|
handedInCount = Number.parseInt(<string>condition.value);
|
|
|
|
isItemHandoverQuest = condition.conditionType === handoverQuestTypes[0];
|
2023-03-03 15:23:46 +00:00
|
|
|
handoverRequirements = condition;
|
|
|
|
|
2024-07-23 11:12:53 -04:00
|
|
|
const profileCounter =
|
|
|
|
handoverQuestRequest.conditionId in pmcData.TaskConditionCounters
|
2024-05-17 15:32:41 -04:00
|
|
|
? pmcData.TaskConditionCounters[handoverQuestRequest.conditionId].value
|
|
|
|
: 0;
|
2023-03-03 15:23:46 +00:00
|
|
|
handedInCount -= profileCounter;
|
|
|
|
|
2024-07-23 11:12:53 -04:00
|
|
|
if (handedInCount <= 0) {
|
2023-11-16 21:42:06 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-23 11:12:53 -04:00
|
|
|
if (isItemHandoverQuest && handedInCount === 0) {
|
2023-03-13 09:34:04 +00:00
|
|
|
return this.showRepeatableQuestInvalidConditionError(handoverQuestRequest, output);
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
2023-11-16 21:42:06 +00:00
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
let totalItemCountToRemove = 0;
|
2024-07-23 11:12:53 -04:00
|
|
|
for (const itemHandover of handoverQuestRequest.items) {
|
2024-05-17 15:32:41 -04:00
|
|
|
const matchingItemInProfile = pmcData.Inventory.items.find((item) => item._id === itemHandover.id);
|
2024-07-23 11:12:53 -04:00
|
|
|
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
|
2023-11-16 21:42:06 +00:00
|
|
|
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;
|
2024-07-23 11:12:53 -04:00
|
|
|
if (itemHandover.count - itemCountToRemove > 0) {
|
2023-03-03 15:23:46 +00:00
|
|
|
// Remove single item with no children
|
2023-11-16 21:42:06 +00:00
|
|
|
this.questHelper.changeItemStack(
|
|
|
|
pmcData,
|
|
|
|
itemHandover.id,
|
|
|
|
itemHandover.count - itemCountToRemove,
|
|
|
|
sessionID,
|
|
|
|
output,
|
|
|
|
);
|
2024-07-23 11:12:53 -04:00
|
|
|
if (totalItemCountToRemove === handedInCount) {
|
2023-03-03 15:23:46 +00:00
|
|
|
break;
|
|
|
|
}
|
2024-07-23 11:12:53 -04:00
|
|
|
} else {
|
2023-03-03 15:23:46 +00:00
|
|
|
// 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
|
2023-11-16 21:42:06 +00:00
|
|
|
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
|
2024-07-23 11:12:53 -04:00
|
|
|
while (index-- > 0) {
|
|
|
|
if (toRemove.includes(pmcData.Inventory.items[index]._id)) {
|
2024-02-09 12:53:44 +00:00
|
|
|
// Remove the item
|
|
|
|
const removedItem = pmcData.Inventory.items.splice(index, 1)[0];
|
|
|
|
|
|
|
|
// If the removed item has a numeric `location` property, re-calculate all the child
|
|
|
|
// element `location` properties of the parent so they are sequential, while retaining order
|
2024-07-23 11:12:53 -04:00
|
|
|
if (typeof removedItem.location === "number") {
|
2024-02-10 16:23:26 +00:00
|
|
|
const childItems = this.itemHelper.findAndReturnChildrenAsItems(
|
|
|
|
pmcData.Inventory.items,
|
|
|
|
removedItem.parentId,
|
|
|
|
);
|
2024-02-09 12:53:44 +00:00
|
|
|
childItems.shift(); // Remove the parent
|
|
|
|
|
|
|
|
// Sort by the current `location` and update
|
2024-05-13 17:03:47 -04:00
|
|
|
childItems.sort((a, b) => (a.location > b.location ? 1 : -1));
|
2024-07-23 11:12:53 -04:00
|
|
|
for (const [index, item] of childItems.entries()) {
|
2024-02-09 12:53:44 +00:00
|
|
|
item.location = index;
|
2024-03-06 19:50:45 +00:00
|
|
|
}
|
2024-02-09 12:53:44 +00:00
|
|
|
}
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-30 21:30:54 +00:00
|
|
|
this.updateProfileTaskConditionCounterValue(
|
2023-11-16 21:42:06 +00:00
|
|
|
pmcData,
|
|
|
|
handoverQuestRequest.conditionId,
|
|
|
|
handoverQuestRequest.qid,
|
|
|
|
totalItemCountToRemove,
|
|
|
|
);
|
2023-03-03 15:23:46 +00:00
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2023-03-13 09:34:04 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2023-11-16 21:42:06 +00:00
|
|
|
protected showRepeatableQuestInvalidConditionError(
|
|
|
|
handoverQuestRequest: IHandoverQuestRequestData,
|
|
|
|
output: IItemEventRouterResponse,
|
2024-07-23 11:12:53 -04:00
|
|
|
): IItemEventRouterResponse {
|
2023-11-16 21:42:06 +00:00
|
|
|
const errorMessage = this.localisationService.getText("repeatable-quest_handover_failed_condition_invalid", {
|
|
|
|
questId: handoverQuestRequest.qid,
|
|
|
|
conditionId: handoverQuestRequest.conditionId,
|
|
|
|
});
|
2023-03-13 09:34:04 +00:00
|
|
|
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
|
|
|
|
*/
|
2023-11-16 21:42:06 +00:00
|
|
|
protected showQuestItemHandoverMatchError(
|
|
|
|
handoverQuestRequest: IHandoverQuestRequestData,
|
2024-09-24 12:47:29 +01:00
|
|
|
itemHandedOver: IItem,
|
2024-01-05 19:52:21 +00:00
|
|
|
handoverRequirements: IQuestCondition,
|
2023-11-16 21:42:06 +00:00
|
|
|
output: IItemEventRouterResponse,
|
2024-07-23 11:12:53 -04:00
|
|
|
): IItemEventRouterResponse {
|
2023-11-16 21:42:06 +00:00
|
|
|
const errorMessage = this.localisationService.getText("quest-handover_wrong_item", {
|
|
|
|
questId: handoverQuestRequest.qid,
|
2024-07-21 00:14:33 +01:00
|
|
|
handedInTpl: itemHandedOver?._tpl ?? "UNKNOWN",
|
2023-12-27 17:15:38 +00:00
|
|
|
requiredTpl: handoverRequirements.target[0],
|
2023-11-16 21:42:06 +00:00
|
|
|
});
|
2023-03-13 09:34:04 +00:00
|
|
|
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
|
|
|
|
*/
|
2023-12-30 21:30:54 +00:00
|
|
|
protected updateProfileTaskConditionCounterValue(
|
2023-11-16 21:42:06 +00:00
|
|
|
pmcData: IPmcData,
|
|
|
|
conditionId: string,
|
|
|
|
questId: string,
|
|
|
|
counterValue: number,
|
2024-07-23 11:12:53 -04:00
|
|
|
): void {
|
|
|
|
if (pmcData.TaskConditionCounters[conditionId] !== undefined) {
|
2023-12-30 21:30:54 +00:00
|
|
|
pmcData.TaskConditionCounters[conditionId].value += counterValue;
|
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-12-30 21:30:54 +00:00
|
|
|
pmcData.TaskConditionCounters[conditionId] = {
|
|
|
|
id: conditionId,
|
|
|
|
sourceId: questId,
|
|
|
|
type: "HandoverItem",
|
2024-02-02 13:54:07 -05:00
|
|
|
value: counterValue,
|
|
|
|
};
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
2024-01-05 15:27:28 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle /client/game/profile/items/moving - QuestFail
|
|
|
|
* @param pmcData Pmc profile
|
|
|
|
* @param request Fail qeust request
|
|
|
|
* @param sessionID Session id
|
|
|
|
* @returns IItemEventRouterResponse
|
|
|
|
*/
|
2024-02-10 16:23:26 +00:00
|
|
|
public failQuest(
|
|
|
|
pmcData: IPmcData,
|
|
|
|
request: IFailQuestRequestData,
|
|
|
|
sessionID: string,
|
|
|
|
output: IItemEventRouterResponse,
|
2024-07-23 11:12:53 -04:00
|
|
|
): IItemEventRouterResponse {
|
2024-03-12 21:29:50 +00:00
|
|
|
this.questHelper.failQuest(pmcData, request, sessionID, output);
|
|
|
|
|
|
|
|
return output;
|
2024-01-05 15:27:28 +00:00
|
|
|
}
|
2023-11-16 21:42:06 +00:00
|
|
|
}
|