2023-03-03 16:23:46 +01:00
import { inject , injectable } from "tsyringe" ;
import { IPmcData } from "../models/eft/common/IPmcData" ;
2023-10-10 13:03:20 +02:00
import { IQuestStatus } from "../models/eft/common/tables/IBotBase" ;
2023-03-03 16:23:46 +01:00
import { Item } from "../models/eft/common/tables/IItem" ;
import {
AvailableForConditions , AvailableForProps , IQuest , Reward
} from "../models/eft/common/tables/IQuest" ;
import { IItemEventRouterResponse } from "../models/eft/itemEvent/IItemEventRouterResponse" ;
import { IAcceptQuestRequestData } from "../models/eft/quests/IAcceptQuestRequestData" ;
import { IFailQuestRequestData } from "../models/eft/quests/IFailQuestRequestData" ;
import { ConfigTypes } from "../models/enums/ConfigTypes" ;
import { MessageType } from "../models/enums/MessageType" ;
import { QuestRewardType } from "../models/enums/QuestRewardType" ;
import { QuestStatus } from "../models/enums/QuestStatus" ;
import { IQuestConfig } from "../models/spt/config/IQuestConfig" ;
import { ILogger } from "../models/spt/utils/ILogger" ;
import { EventOutputHolder } from "../routers/EventOutputHolder" ;
import { ConfigServer } from "../servers/ConfigServer" ;
import { DatabaseServer } from "../servers/DatabaseServer" ;
import { LocaleService } from "../services/LocaleService" ;
import { LocalisationService } from "../services/LocalisationService" ;
2023-07-22 14:02:42 +02:00
import { MailSendService } from "../services/MailSendService" ;
2023-03-03 16:23:46 +01:00
import { HashUtil } from "../utils/HashUtil" ;
import { JsonUtil } from "../utils/JsonUtil" ;
import { TimeUtil } from "../utils/TimeUtil" ;
import { DialogueHelper } from "./DialogueHelper" ;
import { ItemHelper } from "./ItemHelper" ;
import { PaymentHelper } from "./PaymentHelper" ;
import { ProfileHelper } from "./ProfileHelper" ;
2023-10-10 13:03:20 +02:00
import { QuestConditionHelper } from "./QuestConditionHelper" ;
2023-03-03 16:23:46 +01:00
import { RagfairServerHelper } from "./RagfairServerHelper" ;
import { TraderHelper } from "./TraderHelper" ;
@injectable ( )
export class QuestHelper
{
protected questConfig : IQuestConfig ;
constructor (
@inject ( "WinstonLogger" ) protected logger : ILogger ,
@inject ( "JsonUtil" ) protected jsonUtil : JsonUtil ,
@inject ( "TimeUtil" ) protected timeUtil : TimeUtil ,
@inject ( "HashUtil" ) protected hashUtil : HashUtil ,
@inject ( "ItemHelper" ) protected itemHelper : ItemHelper ,
2023-10-10 13:03:20 +02:00
@inject ( "QuestConditionHelper" ) protected questConditionHelper : QuestConditionHelper ,
2023-03-03 16:23:46 +01:00
@inject ( "EventOutputHolder" ) protected eventOutputHolder : EventOutputHolder ,
@inject ( "DatabaseServer" ) protected databaseServer : DatabaseServer ,
@inject ( "LocaleService" ) protected localeService : LocaleService ,
@inject ( "RagfairServerHelper" ) protected ragfairServerHelper : RagfairServerHelper ,
@inject ( "DialogueHelper" ) protected dialogueHelper : DialogueHelper ,
@inject ( "ProfileHelper" ) protected profileHelper : ProfileHelper ,
@inject ( "PaymentHelper" ) protected paymentHelper : PaymentHelper ,
@inject ( "LocalisationService" ) protected localisationService : LocalisationService ,
@inject ( "TraderHelper" ) protected traderHelper : TraderHelper ,
2023-07-22 14:02:42 +02:00
@inject ( "MailSendService" ) protected mailSendService : MailSendService ,
2023-03-03 16:23:46 +01:00
@inject ( "ConfigServer" ) protected configServer : ConfigServer
)
{
this . questConfig = this . configServer . getConfig ( ConfigTypes . QUEST ) ;
}
/ * *
* Get status of a quest in player profile by its id
* @param pmcData Profile to search
2023-03-21 15:19:49 +01:00
* @param questId Quest id to look up
2023-03-03 16:23:46 +01:00
* @returns QuestStatus enum
* /
2023-03-21 15:19:49 +01:00
public getQuestStatus ( pmcData : IPmcData , questId : string ) : QuestStatus
2023-03-03 16:23:46 +01:00
{
2023-05-22 16:50:10 +02:00
const quest = pmcData . Quests ? . find ( q = > q . qid === questId ) ;
2023-03-03 16:23:46 +01:00
2023-03-21 15:19:49 +01:00
return quest
? quest . status
: QuestStatus . Locked ;
2023-03-03 16:23:46 +01:00
}
/ * *
* returns true is the level condition is satisfied
* @param playerLevel Players level
* @param condition Quest condition
* @returns true if player level is greater than or equal to quest
* /
public doesPlayerLevelFulfilCondition ( playerLevel : number , condition : AvailableForConditions ) : boolean
{
if ( condition . _parent === "Level" )
{
switch ( condition . _props . compareMethod )
{
case ">=" :
2023-05-22 17:00:09 +02:00
return playerLevel >= < number > condition . _props . value ;
case ">" :
return playerLevel > < number > condition . _props . value ;
case "<" :
return playerLevel < < number > condition . _props . value ;
case "<=" :
return playerLevel <= < number > condition . _props . value ;
case "=" :
return playerLevel === < number > condition . _props . value ;
2023-03-03 16:23:46 +01:00
default :
2023-07-19 12:00:34 +02:00
this . logger . error ( this . localisationService . getText ( "quest-unable_to_find_compare_condition" , condition . _props . compareMethod ) ) ;
2023-03-03 16:23:46 +01:00
return false ;
}
}
}
/ * *
* Get the quests found in both arrays ( inner join )
* @param before Array of quests # 1
* @param after Array of quests # 2
* @returns Reduction of cartesian product between two quest arrays
* /
public getDeltaQuests ( before : IQuest [ ] , after : IQuest [ ] ) : IQuest [ ]
{
const knownQuestsIds = [ ] ;
for ( const q of before )
{
knownQuestsIds . push ( q . _id ) ;
}
if ( knownQuestsIds . length )
{
return after . filter ( ( q ) = >
{
return knownQuestsIds . indexOf ( q . _id ) === - 1 ;
} ) ;
}
return after ;
}
/ * *
* Increase skill points of a skill on player profile
2023-07-13 11:26:47 +02:00
* Dupe of PlayerService . incrementSkillLevel ( )
2023-03-03 16:23:46 +01:00
* @param sessionID Session id
* @param pmcData Player profile
* @param skillName Name of skill to increase skill points of
* @param progressAmount Amount of skill points to add to skill
* /
public rewardSkillPoints ( sessionID : string , pmcData : IPmcData , skillName : string , progressAmount : number ) : void
{
const indexOfSkillToUpdate = pmcData . Skills . Common . findIndex ( s = > s . Id === skillName ) ;
if ( indexOfSkillToUpdate === - 1 )
{
this . logger . error ( this . localisationService . getText ( "quest-no_skill_found" , skillName ) ) ;
return ;
}
const profileSkill = pmcData . Skills . Common [ indexOfSkillToUpdate ] ;
2023-07-13 11:26:47 +02:00
if ( ! profileSkill )
{
this . logger . error ( this . localisationService . getText ( "quest-no_skill_found" , skillName ) ) ;
return ;
}
2023-03-03 16:23:46 +01:00
profileSkill . Progress += progressAmount ;
profileSkill . LastAccess = this . timeUtil . getTimestamp ( ) ;
}
/ * *
* Get quest name by quest id
* @param questId id to get
* @returns
* /
public getQuestNameFromLocale ( questId : string ) : string
{
const questNameKey = ` ${ questId } name ` ;
return this . localeService . getLocaleDb ( ) [ questNameKey ] ;
}
/ * *
* Check if trader has sufficient loyalty to fulfill quest requirement
* @param questProperties Quest props
* @param profile Player profile
* @returns true if loyalty is high enough to fulfill quest requirement
* /
2023-10-10 13:03:20 +02:00
public traderLoyaltyLevelRequirementCheck ( questProperties : AvailableForProps , profile : IPmcData ) : boolean
{
const requiredLoyaltyLevel = Number ( questProperties . value ) ;
const trader = profile . TradersInfo [ < string > questProperties . target ] ;
if ( ! trader )
{
this . logger . error ( ` Unable to find trader: ${ questProperties . target } in profile ` ) ;
}
return this . compareAvailableForValues ( trader . loyaltyLevel , requiredLoyaltyLevel , questProperties . compareMethod ) ;
}
/ * *
* Check if trader has sufficient standing to fulfill quest requirement
* @param questProperties Quest props
* @param profile Player profile
* @returns true if standing is high enough to fulfill quest requirement
* /
2023-03-03 16:23:46 +01:00
public traderStandingRequirementCheck ( questProperties : AvailableForProps , profile : IPmcData ) : boolean
{
2023-10-10 13:03:20 +02:00
const requiredStanding = Number ( questProperties . value ) ;
const trader = profile . TradersInfo [ < string > questProperties . target ] ;
if ( ! trader )
{
this . logger . error ( ` Unable to find trader: ${ questProperties . target } in profile ` ) ;
}
return this . compareAvailableForValues ( trader . standing , requiredStanding , questProperties . compareMethod ) ;
}
2023-03-03 16:23:46 +01:00
2023-10-10 13:03:20 +02:00
protected compareAvailableForValues ( current : number , required : number , compareMethod : string ) : boolean
{
switch ( compareMethod )
2023-03-03 16:23:46 +01:00
{
case ">=" :
2023-10-10 13:03:20 +02:00
return current >= required ;
2023-03-03 16:23:46 +01:00
case ">" :
2023-10-10 13:03:20 +02:00
return current > required ;
2023-03-03 16:23:46 +01:00
case "<=" :
2023-10-10 13:03:20 +02:00
return current <= required ;
2023-03-03 16:23:46 +01:00
case "<" :
2023-10-10 13:03:20 +02:00
return current < required ;
2023-03-03 16:23:46 +01:00
case "!=" :
2023-10-10 13:03:20 +02:00
return current !== required ;
2023-03-03 16:23:46 +01:00
case "==" :
2023-10-10 13:03:20 +02:00
return current === required ;
2023-03-03 16:23:46 +01:00
default :
2023-10-10 13:03:20 +02:00
this . logger . error ( this . localisationService . getText ( "quest-compare_operator_unhandled" , compareMethod ) ) ;
2023-03-03 16:23:46 +01:00
return false ;
}
}
2023-03-21 15:19:49 +01:00
/ * *
* take reward item from quest and set FiR status + fix stack sizes + fix mod Ids
* @param reward Reward item to fix
* @returns Fixed rewards
* /
2023-03-03 16:23:46 +01:00
protected processReward ( reward : Reward ) : Reward [ ]
{
let rewardItems : Reward [ ] = [ ] ;
let targets : Item [ ] = [ ] ;
const mods : Item [ ] = [ ] ;
for ( const item of reward . items )
{
// reward items are granted Found in Raid status
if ( ! item . upd )
{
item . upd = { } ;
}
item . upd . SpawnedInSession = true ;
// separate base item and mods, fix stacks
if ( item . _id === reward . target )
{
if ( ( item . parentId !== undefined ) && ( item . parentId === "hideout" )
&& ( item . upd !== undefined ) && ( item . upd . StackObjectsCount !== undefined )
&& ( item . upd . StackObjectsCount > 1 ) )
{
item . upd . StackObjectsCount = 1 ;
}
targets = this . itemHelper . splitStack ( item ) ;
// splitStack created new ids for the new stacks. This would destroy the relation to possible children.
// Instead, we reset the id to preserve relations and generate a new id in the downstream loop, where we are also reparenting if required
for ( const target of targets )
{
target . _id = item . _id ;
}
}
else
{
mods . push ( item ) ;
}
}
// Add mods to the base items, fix ids
for ( const target of targets )
{
// This has all the original id relations since we reset the id to the original after the splitStack
const items = [ this . jsonUtil . clone ( target ) ] ;
// Here we generate a new id for the root item
target . _id = this . hashUtil . generate ( ) ;
for ( const mod of mods )
{
items . push ( this . jsonUtil . clone ( mod ) ) ;
}
rewardItems = rewardItems . concat ( < Reward [ ] > this . ragfairServerHelper . reparentPresets ( target , items ) ) ;
}
return rewardItems ;
}
/ * *
* Gets a flat list of reward items for the given quest at a specific state ( e . g . Fail / Success )
* @param quest quest to get rewards for
2023-03-21 15:19:49 +01:00
* @param status Quest status that holds the items ( Started , Success , Fail )
2023-03-03 16:23:46 +01:00
* @returns array of items with the correct maxStack
* /
2023-03-21 15:19:49 +01:00
public getQuestRewardItems ( quest : IQuest , status : QuestStatus ) : Reward [ ]
2023-03-03 16:23:46 +01:00
{
2023-03-21 15:19:49 +01:00
// Iterate over all rewards with the desired status, flatten out items that have a type of Item
const questRewards = quest . rewards [ QuestStatus [ status ] ]
. flatMap ( ( reward : Reward ) = > reward . type === "Item"
? this . processReward ( reward )
: [ ] ) ;
2023-03-03 16:23:46 +01:00
return questRewards ;
}
/ * *
* Look up quest in db by accepted quest id and construct a profile - ready object ready to store in profile
* @param pmcData Player profile
* @param newState State the new quest should be in when returned
* @param acceptedQuest Details of accepted quest from client
* /
2023-10-10 13:03:20 +02:00
public getQuestReadyForProfile ( pmcData : IPmcData , newState : QuestStatus , acceptedQuest : IAcceptQuestRequestData ) : IQuestStatus
2023-03-03 16:23:46 +01:00
{
const existingQuest = pmcData . Quests . find ( q = > q . qid === acceptedQuest . qid ) ;
if ( existingQuest )
{
// Quest exists, update its status
existingQuest . startTime = this . timeUtil . getTimestamp ( ) ;
existingQuest . status = newState ;
existingQuest . statusTimers [ newState ] = this . timeUtil . getTimestamp ( ) ;
2023-10-10 13:03:20 +02:00
existingQuest . completedConditions = [ ] ;
2023-03-03 16:23:46 +01:00
2023-10-10 17:18:55 +02:00
if ( existingQuest . availableAfter )
{
delete existingQuest . availableAfter ;
}
2023-03-03 16:23:46 +01:00
return existingQuest ;
}
// Quest doesn't exists, add it
2023-10-10 13:03:20 +02:00
const newQuest : IQuestStatus = {
2023-03-03 16:23:46 +01:00
qid : acceptedQuest.qid ,
startTime : this.timeUtil.getTimestamp ( ) ,
status : newState ,
statusTimers : { }
} ;
// Check if quest has a prereq to be placed in a 'pending' state
const questDbData = this . getQuestFromDb ( acceptedQuest . qid , pmcData ) ;
const waitTime = questDbData . conditions . AvailableForStart . find ( x = > x . _props . availableAfter > 0 ) ;
if ( waitTime && acceptedQuest . type !== "repeatable" )
{
// Quest should be put into 'pending' state
newQuest . startTime = 0 ;
newQuest . status = QuestStatus . AvailableAfter ; // 9
newQuest . availableAfter = this . timeUtil . getTimestamp ( ) + waitTime . _props . availableAfter ;
}
else
{
newQuest . statusTimers [ newState . toString ( ) ] = this . timeUtil . getTimestamp ( ) ;
newQuest . completedConditions = [ ] ;
}
return newQuest ;
}
/ * *
2023-03-21 15:19:49 +01:00
* Get quests that can be shown to player after starting a quest
* @param startedQuestId Quest started by player
2023-03-03 16:23:46 +01:00
* @param sessionID Session id
2023-03-21 15:19:49 +01:00
* @returns Quests accessible to player incuding newly unlocked quests now quest ( startedQuestId ) was started
2023-03-03 16:23:46 +01:00
* /
2023-10-10 13:03:20 +02:00
public getNewlyAccessibleQuestsWhenStartingQuest ( startedQuestId : string , sessionID : string ) : IQuest [ ]
2023-03-03 16:23:46 +01:00
{
2023-03-21 15:19:49 +01:00
// Get quest acceptance data from profile
2023-10-10 13:03:20 +02:00
const profile : IPmcData = this . profileHelper . getPmcProfile ( sessionID ) ;
const startedQuestInProfile = profile . Quests . find ( x = > x . qid === startedQuestId ) ;
2023-03-03 16:23:46 +01:00
2023-03-21 15:19:49 +01:00
// Get quests that
const eligibleQuests = this . getQuestsFromDb ( ) . filter ( ( quest ) = >
2023-03-03 16:23:46 +01:00
{
2023-03-21 15:19:49 +01:00
// Quest is accessible to player when the accepted quest passed into param is started
// e.g. Quest A passed in, quest B is looped over and has requirement of A to be started, include it
const acceptedQuestCondition = quest . conditions . AvailableForStart . find ( x = >
{
return x . _parent === "Quest"
&& x . _props . target === startedQuestId
&& x . _props . status [ 0 ] === QuestStatus . Started ;
} ) ;
2023-03-03 16:23:46 +01:00
2023-03-21 15:19:49 +01:00
// Not found, skip quest
2023-03-03 16:23:46 +01:00
if ( ! acceptedQuestCondition )
{
return false ;
}
2023-10-10 13:03:20 +02:00
const standingRequirements = this . questConditionHelper . getStandingConditions ( quest . conditions . AvailableForStart ) ;
for ( const condition of standingRequirements )
{
if ( ! this . traderStandingRequirementCheck ( condition . _props , profile ) )
{
return false ;
}
}
const loyaltyRequirements = this . questConditionHelper . getLoyaltyConditions ( quest . conditions . AvailableForStart ) ;
for ( const condition of loyaltyRequirements )
{
if ( ! this . traderLoyaltyLevelRequirementCheck ( condition . _props , profile ) )
{
return false ;
}
}
2023-03-21 15:19:49 +01:00
// Include if quest found in profile and is started or ready to hand in
2023-10-10 13:03:20 +02:00
return startedQuestInProfile && ( [ QuestStatus . Started , QuestStatus . AvailableForFinish ] . includes ( startedQuestInProfile . status ) ) ;
2023-03-03 16:23:46 +01:00
} ) ;
2023-03-21 15:19:49 +01:00
return this . getQuestsWithOnlyLevelRequirementStartCondition ( eligibleQuests ) ;
2023-03-03 16:23:46 +01:00
}
/ * *
2023-03-21 15:19:49 +01:00
* Get quests that can be shown to player after failing a quest
* @param failedQuestId Id of the quest failed by player
* @param sessionId Session id
2023-07-25 20:50:46 +02:00
* @returns IQuest array
2023-03-03 16:23:46 +01:00
* /
2023-03-21 15:19:49 +01:00
public failedUnlocked ( failedQuestId : string , sessionId : string ) : IQuest [ ]
2023-03-03 16:23:46 +01:00
{
2023-03-21 15:19:49 +01:00
const profile = this . profileHelper . getPmcProfile ( sessionId ) ;
const profileQuest = profile . Quests . find ( x = > x . qid === failedQuestId ) ;
2023-03-03 16:23:46 +01:00
const quests = this . getQuestsFromDb ( ) . filter ( ( q ) = >
{
const acceptedQuestCondition = q . conditions . AvailableForStart . find (
c = >
{
return c . _parent === "Quest"
&& c . _props . target === failedQuestId
&& c . _props . status [ 0 ] === QuestStatus . Fail ;
} ) ;
if ( ! acceptedQuestCondition )
{
return false ;
}
return profileQuest && ( profileQuest . status === QuestStatus . Fail ) ;
} ) ;
2023-07-25 20:50:46 +02:00
if ( quests . length === 0 )
{
return quests ;
}
2023-03-03 16:23:46 +01:00
return this . getQuestsWithOnlyLevelRequirementStartCondition ( quests ) ;
}
/ * *
* Adjust quest money rewards by passed in multiplier
* @param quest Quest to multiple money rewards
* @param multiplier Value to adjust money rewards by
2023-10-10 13:03:20 +02:00
* @param questStatus Status of quest to apply money boost to rewards of
2023-03-03 16:23:46 +01:00
* @returns Updated quest
* /
2023-10-10 13:03:20 +02:00
public applyMoneyBoost ( quest : IQuest , multiplier : number , questStatus : QuestStatus ) : IQuest
2023-03-03 16:23:46 +01:00
{
2023-10-10 13:03:20 +02:00
const rewards : Reward [ ] = quest . rewards ? . [ QuestStatus [ questStatus ] ] ? ? [ ] ;
for ( const reward of rewards )
2023-03-03 16:23:46 +01:00
{
if ( reward . type === "Item" )
{
if ( this . paymentHelper . isMoneyTpl ( reward . items [ 0 ] . _tpl ) )
{
reward . items [ 0 ] . upd . StackObjectsCount += Math . round ( reward . items [ 0 ] . upd . StackObjectsCount * multiplier / 100 ) ;
}
}
}
return quest ;
}
/ * *
* Sets the item stack to new value , or delete the item if value <= 0
* // TODO maybe merge this function and the one from customization
* @param pmcData Profile
* @param itemId id of item to adjust stack size of
* @param newStackSize Stack size to adjust to
* @param sessionID Session id
* @param output ItemEvent router response
* /
public changeItemStack ( pmcData : IPmcData , itemId : string , newStackSize : number , sessionID : string , output : IItemEventRouterResponse ) : void
{
const inventoryItemIndex = pmcData . Inventory . items . findIndex ( item = > item . _id === itemId ) ;
if ( inventoryItemIndex < 0 )
{
this . logger . error ( this . localisationService . getText ( "quest-item_not_found_in_inventory" , itemId ) ) ;
return ;
}
if ( newStackSize > 0 )
{
const item = pmcData . Inventory . items [ inventoryItemIndex ] ;
item . upd . StackObjectsCount = newStackSize ;
2023-03-21 15:19:49 +01:00
this . addItemStackSizeChangeIntoEventResponse ( output , sessionID , item ) ;
2023-03-03 16:23:46 +01:00
}
else
{
// this case is probably dead Code right now, since the only calling function
// checks explicitly for Value > 0.
output . profileChanges [ sessionID ] . items . del . push ( { "_id" : itemId } ) ;
pmcData . Inventory . items . splice ( inventoryItemIndex , 1 ) ;
}
}
2023-03-21 15:19:49 +01:00
/ * *
* Add item stack change object into output route event response
* @param output Response to add item change event into
* @param sessionId Session id
* @param item Item that was adjusted
* /
protected addItemStackSizeChangeIntoEventResponse ( output : IItemEventRouterResponse , sessionId : string , item : Item ) : void
{
output . profileChanges [ sessionId ] . items . change . push ( {
"_id" : item . _id ,
"_tpl" : item . _tpl ,
"parentId" : item . parentId ,
"slotId" : item . slotId ,
"location" : item . location ,
"upd" : { "StackObjectsCount" : item . upd . StackObjectsCount }
} ) ;
}
2023-03-03 16:23:46 +01:00
/ * *
* Get quests , strip all requirement conditions except level
* @param quests quests to process
* @returns quest array without conditions
* /
protected getQuestsWithOnlyLevelRequirementStartCondition ( quests : IQuest [ ] ) : IQuest [ ]
{
for ( const i in quests )
{
quests [ i ] = this . getQuestWithOnlyLevelRequirementStartCondition ( quests [ i ] ) ;
}
return quests ;
}
/ * *
* Remove all quest conditions except for level requirement
* @param quest quest to clean
* @returns reset IQuest object
* /
public getQuestWithOnlyLevelRequirementStartCondition ( quest : IQuest ) : IQuest
{
quest = this . jsonUtil . clone ( quest ) ;
quest . conditions . AvailableForStart = quest . conditions . AvailableForStart . filter ( q = > q . _parent === "Level" ) ;
return quest ;
}
/ * *
* Fail a quest in a player profile
* @param pmcData Player profile
* @param failRequest Fail quest request data
* @param sessionID Session id
2023-10-10 13:03:20 +02:00
* @param output Client output
2023-03-03 16:23:46 +01:00
* @returns Item event router response
* /
2023-10-10 13:03:20 +02:00
public failQuest ( pmcData : IPmcData , failRequest : IFailQuestRequestData , sessionID : string , output : IItemEventRouterResponse = null ) : IItemEventRouterResponse
2023-03-03 16:23:46 +01:00
{
// Prepare response to send back client
2023-10-10 13:03:20 +02:00
if ( ! output )
{
output = this . eventOutputHolder . getOutput ( sessionID ) ;
}
2023-03-03 16:23:46 +01:00
this . updateQuestState ( pmcData , QuestStatus . Fail , failRequest . qid ) ;
2023-10-10 13:03:20 +02:00
const questRewards = this . applyQuestReward ( pmcData , failRequest . qid , QuestStatus . Fail , sessionID , output ) ;
2023-03-03 16:23:46 +01:00
// Create a dialog message for completing the quest.
const quest = this . getQuestFromDb ( failRequest . qid , pmcData ) ;
2023-07-22 14:02:42 +02:00
this . mailSendService . sendLocalisedNpcMessageToPlayer (
sessionID ,
this . traderHelper . getTraderById ( quest . traderId ) ,
MessageType . QUEST_FAIL ,
quest . failMessageText ,
questRewards ,
2023-07-22 14:35:49 +02:00
this . timeUtil . getHoursAsSeconds ( this . questConfig . redeemTime )
2023-07-22 14:02:42 +02:00
) ;
2023-03-03 16:23:46 +01:00
2023-10-10 13:03:20 +02:00
output . profileChanges [ sessionID ] . quests . push ( this . failedUnlocked ( failRequest . qid , sessionID ) ) ;
2023-03-03 16:23:46 +01:00
2023-10-10 13:03:20 +02:00
return output ;
2023-03-03 16:23:46 +01:00
}
/ * *
* Get List of All Quests from db
* NOT CLONED
* @returns Array of IQuest objects
* /
public getQuestsFromDb ( ) : IQuest [ ]
{
return Object . values ( this . databaseServer . getTables ( ) . templates . quests ) ;
}
/ * *
* Get quest by id from database ( repeatables are stored in profile , check there if questId not found )
* @param questId Id of quest to find
* @param pmcData Player profile
* @returns IQuest object
* /
public getQuestFromDb ( questId : string , pmcData : IPmcData ) : IQuest
{
let quest = this . databaseServer . getTables ( ) . templates . quests [ questId ] ;
// May be a repeatable quest
if ( ! quest )
{
// Check daily/weekly objects
for ( const repeatableType of pmcData . RepeatableQuests )
{
quest = < IQuest > < unknown > repeatableType . activeQuests . find ( x = > x . _id === questId ) ;
if ( quest )
{
break ;
}
}
}
return quest ;
}
/ * *
* Get a quests startedMessageText key from db , if no startedMessageText key found , use description key instead
* @param startedMessageTextId startedMessageText property from IQuest
* @param questDescriptionId description property from IQuest
* @returns message id
* /
public getMessageIdForQuestStart ( startedMessageTextId : string , questDescriptionId : string ) : string
{
// blank or is a guid, use description instead
const startedMessageText = this . getQuestLocaleIdFromDb ( startedMessageTextId ) ;
if ( ! startedMessageText || startedMessageText . trim ( ) === "" || startedMessageText . toLowerCase ( ) === "test" || startedMessageText . length === 24 )
{
return questDescriptionId ;
}
return startedMessageTextId ;
}
/ * *
* Get the locale Id from locale db for a quest message
* @param questMessageId Quest message id to look up
* @returns Locale Id from locale db
* /
public getQuestLocaleIdFromDb ( questMessageId : string ) : string
{
const locale = this . localeService . getLocaleDb ( ) ;
return locale [ questMessageId ] ;
}
/ * *
* Alter a quests state + Add a record to its status timers object
* @param pmcData Profile to update
* @param newQuestState New state the quest should be in
* @param questId Id of the quest to alter the status of
* /
public updateQuestState ( pmcData : IPmcData , newQuestState : QuestStatus , questId : string ) : void
{
// Find quest in profile, update status to desired status
const questToUpdate = pmcData . Quests . find ( quest = > quest . qid === questId ) ;
if ( questToUpdate )
{
questToUpdate . status = newQuestState ;
questToUpdate . statusTimers [ newQuestState ] = this . timeUtil . getTimestamp ( ) ;
}
}
/ * *
* Give player quest rewards - Skills / exp / trader standing / items / assort unlocks - Returns reward items player earned
* @param pmcData Player profile
* @param questId questId of quest to get rewards for
* @param state State of the quest to get rewards for
* @param sessionId Session id
* @param questResponse Response to send back to client
* @returns Array of reward objects
* /
public applyQuestReward ( pmcData : IPmcData , questId : string , state : QuestStatus , sessionId : string , questResponse : IItemEventRouterResponse ) : Reward [ ]
{
let questDetails = this . getQuestFromDb ( questId , pmcData ) ;
2023-10-10 13:03:20 +02:00
if ( ! questDetails )
{
this . logger . warning ( ` Unable to find quest: ${ questId } from db, unable to give quest rewards ` ) ;
return [ ] ;
}
2023-03-03 16:23:46 +01:00
// Check for and apply intel center money bonus if it exists
2023-10-10 13:03:20 +02:00
const questMoneyRewardBonus = this . getQuestMoneyRewardBonus ( pmcData ) ;
if ( questMoneyRewardBonus > 0 )
2023-03-03 16:23:46 +01:00
{
2023-10-10 13:03:20 +02:00
// Apply additional bonus from hideout skill
questDetails = this . applyMoneyBoost ( questDetails , questMoneyRewardBonus , state ) ; // money = money + (money * intelCenterBonus / 100)
2023-03-03 16:23:46 +01:00
}
// e.g. 'Success' or 'AvailableForFinish'
const questStateAsString = QuestStatus [ state ] ;
for ( const reward of < Reward [ ] > questDetails . rewards [ questStateAsString ] )
{
switch ( reward . type )
{
case QuestRewardType . SKILL :
this . rewardSkillPoints ( sessionId , pmcData , reward . target , Number ( reward . value ) ) ;
break ;
case QuestRewardType . EXPERIENCE :
this . profileHelper . addExperienceToPmc ( sessionId , parseInt ( < string > reward . value ) ) ; // this must occur first as the output object needs to take the modified profile exp value
break ;
case QuestRewardType . TRADER_STANDING :
this . traderHelper . addStandingToTrader ( sessionId , reward . target , parseFloat ( < string > reward . value ) ) ;
break ;
case QuestRewardType . TRADER_UNLOCK :
this . traderHelper . setTraderUnlockedState ( reward . target , true , sessionId ) ;
break ;
case QuestRewardType . ITEM :
// Handled by getQuestRewardItems() below
break ;
case QuestRewardType . ASSORTMENT_UNLOCK :
// Handled elsewhere, TODO: find and say here
break ;
2023-08-06 17:54:31 +02:00
case QuestRewardType . STASH_ROWS :
this . logger . debug ( "Not implemented stash rows reward yet" ) ;
break ;
2023-03-03 16:23:46 +01:00
case QuestRewardType . PRODUCTIONS_SCHEME :
this . findAndAddHideoutProductionIdToProfile ( pmcData , reward , questDetails , sessionId , questResponse ) ;
break ;
default :
this . logger . error ( this . localisationService . getText ( "quest-reward_type_not_handled" , { rewardType : reward.type , questId : questId , questName : questDetails.QuestName } ) ) ;
break ;
}
}
return this . getQuestRewardItems ( questDetails , state ) ;
}
/ * *
* WIP - Find hideout craft id and add to unlockedProductionRecipe array in player profile
* also update client response recipeUnlocked array with craft id
* @param pmcData Player profile
* @param craftUnlockReward Reward item from quest with craft unlock details
* @param questDetails Quest with craft unlock reward
* @param sessionID Session id
* @param response Response to send back to client
* /
protected findAndAddHideoutProductionIdToProfile ( pmcData : IPmcData , craftUnlockReward : Reward , questDetails : IQuest , sessionID : string , response : IItemEventRouterResponse ) : void
{
// Get hideout crafts and find those that match by areatype/required level/end product tpl - hope for just one match
const hideoutProductions = this . databaseServer . getTables ( ) . hideout . production ;
const matchingProductions = hideoutProductions . filter ( x = >
x . areaType === Number . parseInt ( craftUnlockReward . traderId )
2023-08-02 13:57:08 +02:00
&& x . requirements . some ( x = > x . requiredLevel === craftUnlockReward . loyaltyLevel )
2023-03-03 16:23:46 +01:00
&& x . endProduct === craftUnlockReward . items [ 0 ] . _tpl ) ;
2023-08-02 13:57:08 +02:00
// More/less than 1 match, above filtering wasn't strict enough
2023-03-03 16:23:46 +01:00
if ( matchingProductions . length !== 1 )
{
2023-08-02 13:57:08 +02:00
this . logger . error ( this . localisationService . getText ( "quest-unable_to_find_matching_hideout_production" , { questName : questDetails.QuestName , matchCount : matchingProductions.length } ) ) ;
2023-03-03 16:23:46 +01:00
return ;
}
// Add above match to pmc profile + client response
const matchingCraftId = matchingProductions [ 0 ] . _id ;
pmcData . UnlockedInfo . unlockedProductionRecipe . push ( matchingCraftId ) ;
response . profileChanges [ sessionID ] . recipeUnlocked [ matchingCraftId ] = true ;
}
/ * *
2023-10-10 13:03:20 +02:00
* Get players money reward bonus from profile
2023-03-03 16:23:46 +01:00
* @param pmcData player profile
* @returns bonus as a percent
* /
2023-10-10 13:03:20 +02:00
protected getQuestMoneyRewardBonus ( pmcData : IPmcData ) : number
2023-03-03 16:23:46 +01:00
{
// Check player has intel center
2023-10-10 13:03:20 +02:00
const moneyRewardBonuses = pmcData . Bonuses . filter ( x = > x . type === "QuestMoneyReward" ) ;
if ( ! moneyRewardBonuses )
2023-03-03 16:23:46 +01:00
{
2023-10-10 13:03:20 +02:00
return 0 ;
}
2023-03-03 16:23:46 +01:00
2023-10-10 13:03:20 +02:00
// Get a total of the quest money rewards
let moneyRewardBonus = moneyRewardBonuses . reduce ( ( acc , cur ) = > acc + cur . value , 0 ) ;
// Apply hideout management bonus to money reward (up to 51% bonus)
const hideoutManagementSkill = pmcData . Skills . Common . find ( x = > x . Id === "HideoutManagement" ) ;
if ( hideoutManagementSkill )
{
moneyRewardBonus *= ( 1 + ( hideoutManagementSkill . Progress / 10000 ) ) ; // 5100 becomes 0.51, add 1 to it, 1.51, multiply the moneyreward bonus by it (e.g. 15 x 51)
2023-03-03 16:23:46 +01:00
}
2023-10-10 13:03:20 +02:00
return moneyRewardBonus ;
2023-03-03 16:23:46 +01:00
}
/ * *
2023-10-10 13:03:20 +02:00
* Find quest with 'findItem' condition that needs the item tpl be handed in
2023-03-03 16:23:46 +01:00
* @param itemTpl item tpl to look for
2023-10-10 13:03:20 +02:00
* @param questIds Quests to search through for the findItem condition
* @returns quest id with 'FindItem' condition id
2023-03-03 16:23:46 +01:00
* /
2023-10-10 13:03:20 +02:00
public getFindItemConditionByQuestItem ( itemTpl : string , questIds : string [ ] , allQuests : IQuest [ ] ) : Record < string , string >
2023-03-03 16:23:46 +01:00
{
2023-10-10 13:03:20 +02:00
const result : Record < string , string > = { } ;
for ( const questId of questIds )
2023-03-03 16:23:46 +01:00
{
2023-10-10 13:03:20 +02:00
const questInDb = allQuests . find ( x = > x . _id === questId ) ;
if ( ! questInDb )
{
this . logger . warning ( ` Unable to find quest: ${ questId } in db, cannot get 'FindItem' condition, skipping ` ) ;
continue ;
}
const condition = questInDb . conditions . AvailableForFinish . find ( c = > c . _parent === "FindItem" && c . _props ? . target ? . includes ( itemTpl ) ) ;
2023-03-03 16:23:46 +01:00
if ( condition )
{
2023-10-10 13:03:20 +02:00
result [ questId ] = condition . _props . id ;
break ;
2023-03-03 16:23:46 +01:00
}
}
2023-03-03 18:53:28 +01:00
return result ;
2023-03-03 16:23:46 +01:00
}
/ * *
* Add all quests to a profile with the provided statuses
* @param pmcProfile profile to update
* @param statuses statuses quests should have
* /
public addAllQuestsToProfile ( pmcProfile : IPmcData , statuses : QuestStatus [ ] ) : void
{
// Iterate over all quests in db
const quests = this . databaseServer . getTables ( ) . templates . quests ;
for ( const questKey in quests )
{
// Quest from db matches quests in profile, skip
const questData = quests [ questKey ] ;
if ( pmcProfile . Quests . find ( x = > x . qid === questData . _id ) )
{
continue ;
}
const statusesDict = { } ;
for ( const status of statuses )
{
statusesDict [ status ] = this . timeUtil . getTimestamp ( ) ;
}
2023-10-10 13:03:20 +02:00
const questRecordToAdd : IQuestStatus = {
2023-03-03 16:23:46 +01:00
qid : questKey ,
2023-10-10 13:03:20 +02:00
startTime : this.timeUtil.getTimestamp ( ) ,
2023-03-03 16:23:46 +01:00
status : statuses [ statuses . length - 1 ] ,
statusTimers : statusesDict ,
completedConditions : [ ] ,
availableAfter : 0
} ;
2023-10-10 13:03:20 +02:00
if ( pmcProfile . Quests . some ( x = > x . qid === questKey ) )
{
// Update existing
const existingQuest = pmcProfile . Quests . find ( x = > x . qid === questKey ) ;
existingQuest . status = questRecordToAdd . status ;
existingQuest . statusTimers = questRecordToAdd . statusTimers ;
}
else
{
// Add new
pmcProfile . Quests . push ( questRecordToAdd ) ;
}
2023-03-03 16:23:46 +01:00
}
}
}