0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:50:43 -05:00

Using live data, improved emulation accuracy of repeatable quest system

This commit is contained in:
Chomp 2024-12-09 23:23:31 +00:00
parent a15a28e460
commit 2abf216a07
7 changed files with 199 additions and 90 deletions

View File

@ -28,7 +28,10 @@
"value": 1, "value": 1,
"type": "Elimination", "type": "Elimination",
"oneSessionOnly": false, "oneSessionOnly": false,
"completeInSeconds": 0,
"doNotResetIfCounterCompleted": false, "doNotResetIfCounterCompleted": false,
"isResetOnConditionFailed": false,
"isNecessary": false,
"counter": { "counter": {
"id": "618c1de4d4cd91439f3de4ac", "id": "618c1de4d4cd91439f3de4ac",
"conditions": [{ "conditions": [{
@ -74,13 +77,26 @@
"acceptPlayerMessage": "{templateId} acceptPlayerMessage {traderId}", "acceptPlayerMessage": "{templateId} acceptPlayerMessage {traderId}",
"declinePlayerMessage": "{templateId} declinePlayerMessage {traderId}", "declinePlayerMessage": "{templateId} declinePlayerMessage {traderId}",
"completePlayerMessage": "{templateId} completePlayerMessage {traderId}", "completePlayerMessage": "{templateId} completePlayerMessage {traderId}",
"templateId": "{templateId}", "status": 0,
"acceptanceAndFinishingSource": "eft",
"progressSource": "eft",
"rankingModes": [],
"gameModes": [],
"arenaLocations": [],
"changeCost": [{ "changeCost": [{
"templateId": "5449016a4bdc2d6f028b456f", "templateId": "5449016a4bdc2d6f028b456f",
"count": 5000 "count": 5000
} }
], ],
"changeStandingCost": 0 "changeStandingCost": 0,
"questStatus": {
"id": "mongoId",
"uid": "playerId",
"qid": "questId",
"startTime": 0,
"status": 1,
"statusTimers": {}
}
}, },
"Completion": { "Completion": {
"_id": "61943a75eb60e11b7965cdbf4", "_id": "61943a75eb60e11b7965cdbf4",
@ -114,13 +130,26 @@
"acceptPlayerMessage": "{templateId} acceptPlayerMessage {traderId}", "acceptPlayerMessage": "{templateId} acceptPlayerMessage {traderId}",
"declinePlayerMessage": "{templateId} declinePlayerMessage {traderId}", "declinePlayerMessage": "{templateId} declinePlayerMessage {traderId}",
"completePlayerMessage": "{templateId} completePlayerMessage {traderId}", "completePlayerMessage": "{templateId} completePlayerMessage {traderId}",
"templateId": "{templateId}", "status": 0,
"acceptanceAndFinishingSource": "eft",
"progressSource": "eft",
"rankingModes": [],
"gameModes": [],
"arenaLocations": [],
"changeCost": [{ "changeCost": [{
"templateId": "5449016a4bdc2d6f028b456f", "templateId": "5449016a4bdc2d6f028b456f",
"count": 5000 "count": 5000
} }
], ],
"changeStandingCost": 0 "changeStandingCost": 0,
"questStatus": {
"id": "mongoId",
"uid": "playerId",
"qid": "questId",
"startTime": 0,
"status": 1,
"statusTimers": {}
}
}, },
"Exploration": { "Exploration": {
"_id": "65947c6afb90e7fcb40f8d684", "_id": "65947c6afb90e7fcb40f8d684",
@ -185,13 +214,26 @@
"acceptPlayerMessage": "{templateId} acceptPlayerMessage {traderId}", "acceptPlayerMessage": "{templateId} acceptPlayerMessage {traderId}",
"declinePlayerMessage": "{templateId} declinePlayerMessage {traderId}", "declinePlayerMessage": "{templateId} declinePlayerMessage {traderId}",
"completePlayerMessage": "{templateId} completePlayerMessage {traderId}", "completePlayerMessage": "{templateId} completePlayerMessage {traderId}",
"templateId": "{templateId}", "status": 0,
"acceptanceAndFinishingSource": "eft",
"progressSource": "eft",
"rankingModes": [],
"gameModes": [],
"arenaLocations": [],
"changeCost": [{ "changeCost": [{
"templateId": "5449016a4bdc2d6f028b456f", "templateId": "5449016a4bdc2d6f028b456f",
"count": 5000 "count": 5000
} }
], ],
"changeStandingCost": 0 "changeStandingCost": 0,
"questStatus": {
"id": "mongoId",
"uid": "playerId",
"qid": "questId",
"startTime": 0,
"status": 1,
"statusTimers": {}
}
}, },
"Pickup": { "Pickup": {
"_id": "64cfb3818db9f48b3f0b0a759", "_id": "64cfb3818db9f48b3f0b0a759",
@ -217,7 +259,7 @@
"dynamicLocale": false, "dynamicLocale": false,
"index": 0, "index": 0,
"visibilityConditions": [], "visibilityConditions": [],
"globalQuestCounterId": null, "globalQuestCounterId": "",
"target": ["5b47574386f77428ca22b336"], "target": ["5b47574386f77428ca22b336"],
"value": 7, "value": 7,
"minDurability": 0, "minDurability": 0,
@ -233,7 +275,7 @@
"dynamicLocale": true, "dynamicLocale": true,
"index": 0, "index": 0,
"visibilityConditions": [], "visibilityConditions": [],
"globalQuestCounterId": null, "globalQuestCounterId": "",
"value": 1, "value": 1,
"type": "PickUp", "type": "PickUp",
"completeInSeconds": 0, "completeInSeconds": 0,
@ -276,13 +318,26 @@
"acceptPlayerMessage": "{templateId} acceptPlayerMessage {traderId}", "acceptPlayerMessage": "{templateId} acceptPlayerMessage {traderId}",
"declinePlayerMessage": "{templateId} declinePlayerMessage {traderId}", "declinePlayerMessage": "{templateId} declinePlayerMessage {traderId}",
"completePlayerMessage": "{templateId} completePlayerMessage {traderId}", "completePlayerMessage": "{templateId} completePlayerMessage {traderId}",
"templateId": "{templateId}", "status": 0,
"acceptanceAndFinishingSource": "eft",
"progressSource": "eft",
"rankingModes": [],
"gameModes": [],
"arenaLocations": [],
"changeCost": [{ "changeCost": [{
"templateId": "5449016a4bdc2d6f028b456f", "templateId": "5449016a4bdc2d6f028b456f",
"count": 12000 "count": 12000
} }
], ],
"changeStandingCost": 0 "changeStandingCost": 0,
"questStatus": {
"id": "mongoId",
"uid": "playerId",
"qid": "questId",
"startTime": 0,
"status": 1,
"statusTimers": {}
}
} }
}, },
"rewards": { "rewards": {

View File

@ -174,6 +174,7 @@ export class QuestController {
} }
/** /**
* TODO - Move this code into RepeatableQuestController
* Handle the client accepting a repeatable quest and starting it * Handle the client accepting a repeatable quest and starting it
* Send starting rewards if any to player and * Send starting rewards if any to player and
* Send start notification if any to player * Send start notification if any to player
@ -221,50 +222,11 @@ export class QuestController {
fullProfile.characters.scav.Quests.push(newRepeatableQuest); fullProfile.characters.scav.Quests.push(newRepeatableQuest);
} }
const response = this.createAcceptedQuestClientResponse(sessionID, pmcData, repeatableQuestProfile); const response = this.eventOutputHolder.getOutput(sessionID);
return response; return response;
} }
protected createAcceptedQuestClientResponse(
sessionID: string,
pmcData: IPmcData,
repeatableQuestProfile: IRepeatableQuest,
): IItemEventRouterResponse {
const repeatableSettings = pmcData.RepeatableQuests.find(
(quest) => quest.name === repeatableQuestProfile.sptRepatableGroupName,
);
const change = {};
change[repeatableQuestProfile._id] = repeatableSettings.changeRequirement[repeatableQuestProfile._id];
const repeatableData: IPmcDataRepeatableQuest = {
id:
repeatableSettings.id ??
this.questConfig.repeatableQuests.find(
(repeatableQuest) => repeatableQuest.name === repeatableQuestProfile.sptRepatableGroupName,
).id,
name: repeatableSettings.name,
endTime: repeatableSettings.endTime,
changeRequirement: change,
activeQuests: [repeatableQuestProfile],
inactiveQuests: [],
freeChanges: repeatableSettings.freeChanges,
freeChangesAvailable: repeatableSettings.freeChangesAvailable,
};
// Nullguard
const acceptQuestResponse = this.eventOutputHolder.getOutput(sessionID);
if (!acceptQuestResponse.profileChanges[sessionID].repeatableQuests) {
acceptQuestResponse.profileChanges[sessionID].repeatableQuests = [];
}
// Add constructed objet into response
acceptQuestResponse.profileChanges[sessionID].repeatableQuests.push(repeatableData);
return acceptQuestResponse;
}
/** /**
* Look for an accepted quest inside player profile, return matching * Look for an accepted quest inside player profile, return matching
* @param pmcData Profile to search through * @param pmcData Profile to search through

View File

@ -129,6 +129,7 @@ export class RepeatableQuestController {
let lifeline = 0; let lifeline = 0;
while (!quest && questTypePool.types.length > 0) { while (!quest && questTypePool.types.length > 0) {
quest = this.repeatableQuestGenerator.generateRepeatableQuest( quest = this.repeatableQuestGenerator.generateRepeatableQuest(
sessionID,
pmcData.Info.Level, pmcData.Info.Level,
pmcData.TradersInfo, pmcData.TradersInfo,
questTypePool, questTypePool,
@ -487,11 +488,11 @@ export class RepeatableQuestController {
const fullProfile = this.profileHelper.getFullProfile(sessionID); const fullProfile = this.profileHelper.getFullProfile(sessionID);
// Check for existing quest in (daily/weekly/scav arrays) // Check for existing quest in (daily/weekly/scav arrays)
const { quest: questToReplace, repeatableType: repeatablesInProfile } = this.getRepeatableById( const { quest: questToReplace, repeatableType: repeatablesOfTypeInProfile } = this.getRepeatableById(
changeRequest.qid, changeRequest.qid,
pmcData, pmcData,
); );
if (!repeatablesInProfile || !questToReplace) { if (!repeatablesOfTypeInProfile || !questToReplace) {
// Unable to find quest being replaced // Unable to find quest being replaced
const message = this.localisationService.getText("quest-unable_to_find_repeatable_to_replace"); const message = this.localisationService.getText("quest-unable_to_find_repeatable_to_replace");
this.logger.error(message); this.logger.error(message);
@ -500,25 +501,27 @@ export class RepeatableQuestController {
} }
// Subtype name of quest - daily/weekly/scav // Subtype name of quest - daily/weekly/scav
const repeatableTypeLower = repeatablesInProfile.name.toLowerCase(); const repeatableTypeLower = repeatablesOfTypeInProfile.name.toLowerCase();
// Save for later standing loss calculation // Save for later standing loss calculation
const replacedQuestTraderId = questToReplace.traderId; const replacedQuestTraderId = questToReplace.traderId;
// Update active quests to exclude the quest we're replacing // Update active quests to exclude the quest we're replacing
repeatablesInProfile.activeQuests = repeatablesInProfile.activeQuests.filter( repeatablesOfTypeInProfile.activeQuests = repeatablesOfTypeInProfile.activeQuests.filter(
(quest) => quest._id !== changeRequest.qid, (quest) => quest._id !== changeRequest.qid,
); );
// Save for later cost calculation // Save for later cost calculations
const previousChangeRequirement = this.cloner.clone(repeatablesInProfile.changeRequirement[changeRequest.qid]); const previousChangeRequirement = this.cloner.clone(
repeatablesOfTypeInProfile.changeRequirement[changeRequest.qid],
);
// Delete the replaced quest change requrement as we're going to replace it // Delete the replaced quest change requirement data as we're going to add new data below
delete repeatablesInProfile.changeRequirement[changeRequest.qid]; delete repeatablesOfTypeInProfile.changeRequirement[changeRequest.qid];
// Get config for this repeatable sub-type (daily/weekly/scav) // Get config for this repeatable sub-type (daily/weekly/scav)
const repeatableConfig = this.questConfig.repeatableQuests.find( const repeatableConfig = this.questConfig.repeatableQuests.find(
(config) => config.name === repeatablesInProfile.name, (config) => config.name === repeatablesOfTypeInProfile.name,
); );
// If the configuration dictates to replace with the same quest type, adjust the available quest types // If the configuration dictates to replace with the same quest type, adjust the available quest types
@ -535,7 +538,12 @@ export class RepeatableQuestController {
// Generate meta-data for what type/levelrange of quests can be generated for player // Generate meta-data for what type/levelrange of quests can be generated for player
const allowedQuestTypes = this.generateQuestPool(repeatableConfig, pmcData.Info.Level); const allowedQuestTypes = this.generateQuestPool(repeatableConfig, pmcData.Info.Level);
const newRepeatableQuest = this.attemptToGenerateRepeatableQuest(pmcData, allowedQuestTypes, repeatableConfig); const newRepeatableQuest = this.attemptToGenerateRepeatableQuest(
sessionID,
pmcData,
allowedQuestTypes,
repeatableConfig,
);
if (!newRepeatableQuest) { if (!newRepeatableQuest) {
// Unable to find quest being replaced // Unable to find quest being replaced
const message = `Unable to generate repeatable quest of type: ${repeatableTypeLower} to replace trader: ${replacedQuestTraderId} quest ${changeRequest.qid}`; const message = `Unable to generate repeatable quest of type: ${repeatableTypeLower} to replace trader: ${replacedQuestTraderId} quest ${changeRequest.qid}`;
@ -546,7 +554,7 @@ export class RepeatableQuestController {
// Add newly generated quest to daily/weekly/scav type array // Add newly generated quest to daily/weekly/scav type array
newRepeatableQuest.side = repeatableConfig.side; newRepeatableQuest.side = repeatableConfig.side;
repeatablesInProfile.activeQuests.push(newRepeatableQuest); repeatablesOfTypeInProfile.activeQuests.push(newRepeatableQuest);
this.logger.debug( this.logger.debug(
`Removing: ${repeatableConfig.name} quest: ${questToReplace._id} from trader: ${questToReplace.traderId} as its been replaced`, `Removing: ${repeatableConfig.name} quest: ${questToReplace._id} from trader: ${questToReplace.traderId} as its been replaced`,
@ -562,13 +570,17 @@ export class RepeatableQuestController {
); );
// Add new quests replacement cost to profile // Add new quests replacement cost to profile
repeatablesInProfile.changeRequirement[newRepeatableQuest._id] = { repeatablesOfTypeInProfile.changeRequirement[newRepeatableQuest._id] = {
changeCost: newRepeatableQuest.changeCost, changeCost: newRepeatableQuest.changeCost,
changeStandingCost: this.randomUtil.getArrayValue([0, 0.01]), changeStandingCost: this.randomUtil.getArrayValue([0, 0.01]),
}; };
// Check if we should charge player for replacing quest // Check if we should charge player for replacing quest
const isFreeToReplace = this.useFreeRefreshIfAvailable(fullProfile, repeatablesInProfile, repeatableTypeLower); const isFreeToReplace = this.useFreeRefreshIfAvailable(
fullProfile,
repeatablesOfTypeInProfile,
repeatableTypeLower,
);
if (!isFreeToReplace) { if (!isFreeToReplace) {
// Reduce standing with trader for not doing their quest // Reduce standing with trader for not doing their quest
const traderOfReplacedQuest = pmcData.TradersInfo[replacedQuestTraderId]; const traderOfReplacedQuest = pmcData.TradersInfo[replacedQuestTraderId];
@ -586,7 +598,7 @@ export class RepeatableQuestController {
} }
// Clone data before we send it to client // Clone data before we send it to client
const repeatableToChangeClone = this.cloner.clone(repeatablesInProfile); const repeatableToChangeClone = this.cloner.clone(repeatablesOfTypeInProfile);
// Purge inactive repeatables // Purge inactive repeatables
repeatableToChangeClone.inactiveQuests = []; repeatableToChangeClone.inactiveQuests = [];
@ -622,6 +634,7 @@ export class RepeatableQuestController {
} }
protected attemptToGenerateRepeatableQuest( protected attemptToGenerateRepeatableQuest(
sessionId: string,
pmcData: IPmcData, pmcData: IPmcData,
questTypePool: IQuestTypePool, questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig, repeatableConfig: IRepeatableQuestConfig,
@ -631,6 +644,7 @@ export class RepeatableQuestController {
let attempts = 0; let attempts = 0;
while (attempts < maxAttempts && questTypePool.types.length > 0) { while (attempts < maxAttempts && questTypePool.types.length > 0) {
newRepeatableQuest = this.repeatableQuestGenerator.generateRepeatableQuest( newRepeatableQuest = this.repeatableQuestGenerator.generateRepeatableQuest(
sessionId,
pmcData.Info.Level, pmcData.Info.Level,
pmcData.TradersInfo, pmcData.TradersInfo,
questTypePool, questTypePool,

View File

@ -50,6 +50,7 @@ export class RepeatableQuestGenerator {
/** /**
* This method is called by /GetClientRepeatableQuests/ and creates one element of quest type format (see assets/database/templates/repeatableQuests.json). * This method is called by /GetClientRepeatableQuests/ and creates one element of quest type format (see assets/database/templates/repeatableQuests.json).
* It randomly draws a quest type (currently Elimination, Completion or Exploration) as well as a trader who is providing the quest * It randomly draws a quest type (currently Elimination, Completion or Exploration) as well as a trader who is providing the quest
* @param sessionId Session id
* @param pmcLevel Player's level for requested items and reward generation * @param pmcLevel Player's level for requested items and reward generation
* @param pmcTraderInfo Players traper standing/rep levels * @param pmcTraderInfo Players traper standing/rep levels
* @param questTypePool Possible quest types pool * @param questTypePool Possible quest types pool
@ -57,6 +58,7 @@ export class RepeatableQuestGenerator {
* @returns IRepeatableQuest * @returns IRepeatableQuest
*/ */
public generateRepeatableQuest( public generateRepeatableQuest(
sessionId: string,
pmcLevel: number, pmcLevel: number,
pmcTraderInfo: Record<string, ITraderInfo>, pmcTraderInfo: Record<string, ITraderInfo>,
questTypePool: IQuestTypePool, questTypePool: IQuestTypePool,
@ -64,7 +66,7 @@ export class RepeatableQuestGenerator {
): IRepeatableQuest { ): IRepeatableQuest {
const questType = this.randomUtil.drawRandomFromList<string>(questTypePool.types)[0]; const questType = this.randomUtil.drawRandomFromList<string>(questTypePool.types)[0];
// get traders from whitelist and filter by quest type availability // Get traders from whitelist and filter by quest type availability
let traders = repeatableConfig.traderWhitelist let traders = repeatableConfig.traderWhitelist
.filter((x) => x.questTypes.includes(questType)) .filter((x) => x.questTypes.includes(questType))
.map((x) => x.traderId); .map((x) => x.traderId);
@ -74,13 +76,13 @@ export class RepeatableQuestGenerator {
switch (questType) { switch (questType) {
case "Elimination": case "Elimination":
return this.generateEliminationQuest(pmcLevel, traderId, questTypePool, repeatableConfig); return this.generateEliminationQuest(sessionId, pmcLevel, traderId, questTypePool, repeatableConfig);
case "Completion": case "Completion":
return this.generateCompletionQuest(pmcLevel, traderId, repeatableConfig); return this.generateCompletionQuest(sessionId, pmcLevel, traderId, repeatableConfig);
case "Exploration": case "Exploration":
return this.generateExplorationQuest(pmcLevel, traderId, questTypePool, repeatableConfig); return this.generateExplorationQuest(sessionId, pmcLevel, traderId, questTypePool, repeatableConfig);
case "Pickup": case "Pickup":
return this.generatePickupQuest(pmcLevel, traderId, questTypePool, repeatableConfig); return this.generatePickupQuest(sessionId, pmcLevel, traderId, questTypePool, repeatableConfig);
default: default:
throw new Error(`Unknown mission type ${questType}. Should never be here!`); throw new Error(`Unknown mission type ${questType}. Should never be here!`);
} }
@ -95,6 +97,7 @@ export class RepeatableQuestGenerator {
* @returns Object of quest type format for "Elimination" (see assets/database/templates/repeatableQuests.json) * @returns Object of quest type format for "Elimination" (see assets/database/templates/repeatableQuests.json)
*/ */
protected generateEliminationQuest( protected generateEliminationQuest(
sessionid: string,
pmcLevel: number, pmcLevel: number,
traderId: string, traderId: string,
questTypePool: IQuestTypePool, questTypePool: IQuestTypePool,
@ -297,7 +300,7 @@ export class RepeatableQuestGenerator {
// crazy maximum difficulty will lead to a higher difficulty reward gain factor than 1 // crazy maximum difficulty will lead to a higher difficulty reward gain factor than 1
const difficulty = this.mathUtil.mapToRange(curDifficulty, minDifficulty, maxDifficulty, 0.5, 2); const difficulty = this.mathUtil.mapToRange(curDifficulty, minDifficulty, maxDifficulty, 0.5, 2);
const quest = this.generateRepeatableTemplate("Elimination", traderId, repeatableConfig.side); const quest = this.generateRepeatableTemplate("Elimination", traderId, repeatableConfig.side, sessionid);
// ASSUMPTION: All fence quests are for scavs // ASSUMPTION: All fence quests are for scavs
if (traderId === Traders.FENCE) { if (traderId === Traders.FENCE) {
@ -396,10 +399,13 @@ export class RepeatableQuestGenerator {
allowedWeaponCategory: string, allowedWeaponCategory: string,
): IQuestConditionCounterCondition { ): IQuestConditionCounterCondition {
const killConditionProps: IQuestConditionCounterCondition = { const killConditionProps: IQuestConditionCounterCondition = {
target: target,
value: 1,
id: this.objectId.generate(), id: this.objectId.generate(),
dynamicLocale: true, dynamicLocale: true,
target: target, // e,g, "AnyPmc"
value: 1,
resetOnSessionEnd: false,
enemyHealthEffects: [],
daytime: { from: 0, to: 0 },
conditionType: "Kills", conditionType: "Kills",
}; };
@ -441,6 +447,7 @@ export class RepeatableQuestGenerator {
* @returns {object} object of quest type format for "Completion" (see assets/database/templates/repeatableQuests.json) * @returns {object} object of quest type format for "Completion" (see assets/database/templates/repeatableQuests.json)
*/ */
protected generateCompletionQuest( protected generateCompletionQuest(
sessionId: string,
pmcLevel: number, pmcLevel: number,
traderId: string, traderId: string,
repeatableConfig: IRepeatableQuestConfig, repeatableConfig: IRepeatableQuestConfig,
@ -449,7 +456,7 @@ export class RepeatableQuestGenerator {
const levelsConfig = repeatableConfig.rewardScaling.levels; const levelsConfig = repeatableConfig.rewardScaling.levels;
const roublesConfig = repeatableConfig.rewardScaling.roubles; const roublesConfig = repeatableConfig.rewardScaling.roubles;
const quest = this.generateRepeatableTemplate("Completion", traderId, repeatableConfig.side); const quest = this.generateRepeatableTemplate("Completion", traderId, repeatableConfig.side, sessionId);
// Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existant" // Filter the items.json items to items the player must retrieve to complete quest: shouldn't be a quest item or "non-existant"
const possibleItemsToRetrievePool = this.repeatableQuestRewardGenerator.getRewardableItems( const possibleItemsToRetrievePool = this.repeatableQuestRewardGenerator.getRewardableItems(
@ -626,16 +633,18 @@ export class RepeatableQuestGenerator {
return { return {
id: this.objectId.generate(), id: this.objectId.generate(),
index: 0,
parentId: "", parentId: "",
dynamicLocale: true, dynamicLocale: true,
index: 0,
visibilityConditions: [], visibilityConditions: [],
globalQuestCounterId: "",
target: [itemTpl], target: [itemTpl],
value: value, value: value,
minDurability: minDurability, minDurability: minDurability,
maxDurability: 100, maxDurability: 100,
dogtagLevel: 0, dogtagLevel: 0,
onlyFoundInRaid: onlyFoundInRaid, onlyFoundInRaid: onlyFoundInRaid,
isEncoded: false,
conditionType: "HandoverItem", conditionType: "HandoverItem",
}; };
} }
@ -650,6 +659,7 @@ export class RepeatableQuestGenerator {
* @returns {object} object of quest type format for "Exploration" (see assets/database/templates/repeatableQuests.json) * @returns {object} object of quest type format for "Exploration" (see assets/database/templates/repeatableQuests.json)
*/ */
protected generateExplorationQuest( protected generateExplorationQuest(
sessionId: string,
pmcLevel: number, pmcLevel: number,
traderId: string, traderId: string,
questTypePool: IQuestTypePool, questTypePool: IQuestTypePool,
@ -679,19 +689,19 @@ export class RepeatableQuestGenerator {
: explorationConfig.maxExtracts + 1; : explorationConfig.maxExtracts + 1;
const numExtracts = this.randomUtil.randInt(1, exitTimesMax); const numExtracts = this.randomUtil.randInt(1, exitTimesMax);
const quest = this.generateRepeatableTemplate("Exploration", traderId, repeatableConfig.side); const quest = this.generateRepeatableTemplate("Exploration", traderId, repeatableConfig.side, sessionId);
const exitStatusCondition: IQuestConditionCounterCondition = { const exitStatusCondition: IQuestConditionCounterCondition = {
conditionType: "ExitStatus",
id: this.objectId.generate(), id: this.objectId.generate(),
dynamicLocale: true, dynamicLocale: true,
status: ["Survived"], status: ["Survived"],
conditionType: "ExitStatus",
}; };
const locationCondition: IQuestConditionCounterCondition = { const locationCondition: IQuestConditionCounterCondition = {
conditionType: "Location",
id: this.objectId.generate(), id: this.objectId.generate(),
dynamicLocale: true, dynamicLocale: true,
target: locationTarget, target: locationTarget,
conditionType: "Location",
}; };
quest.conditions.AvailableForFinish[0].counter.id = this.objectId.generate(); quest.conditions.AvailableForFinish[0].counter.id = this.objectId.generate();
@ -757,6 +767,7 @@ export class RepeatableQuestGenerator {
} }
protected generatePickupQuest( protected generatePickupQuest(
sessionId: string,
pmcLevel: number, pmcLevel: number,
traderId: string, traderId: string,
questTypePool: IQuestTypePool, questTypePool: IQuestTypePool,
@ -764,7 +775,7 @@ export class RepeatableQuestGenerator {
): IRepeatableQuest { ): IRepeatableQuest {
const pickupConfig = repeatableConfig.questConfig.Pickup; const pickupConfig = repeatableConfig.questConfig.Pickup;
const quest = this.generateRepeatableTemplate("Pickup", traderId, repeatableConfig.side); const quest = this.generateRepeatableTemplate("Pickup", traderId, repeatableConfig.side, sessionId);
const itemTypeToFetchWithCount = this.randomUtil.getArrayValue(pickupConfig.ItemTypeToFetchWithMaxCount); const itemTypeToFetchWithCount = this.randomUtil.getArrayValue(pickupConfig.ItemTypeToFetchWithMaxCount);
const itemCountToFetch = this.randomUtil.randInt( const itemCountToFetch = this.randomUtil.randInt(
@ -819,7 +830,12 @@ export class RepeatableQuestGenerator {
* @returns {object} Exit condition * @returns {object} Exit condition
*/ */
protected generateExplorationExitCondition(exit: IExit): IQuestConditionCounterCondition { protected generateExplorationExitCondition(exit: IExit): IQuestConditionCounterCondition {
return { conditionType: "ExitName", exitName: exit.Name, id: this.objectId.generate(), dynamicLocale: true }; return {
id: this.objectId.generate(),
dynamicLocale: true,
exitName: exit.Name,
conditionType: "ExitName",
};
} }
/** /**
@ -833,7 +849,12 @@ export class RepeatableQuestGenerator {
* (needs to be filled with reward and conditions by called to make a valid quest) * (needs to be filled with reward and conditions by called to make a valid quest)
*/ */
// @Incomplete: define Type for "type". // @Incomplete: define Type for "type".
protected generateRepeatableTemplate(type: string, traderId: string, side: string): IRepeatableQuest { protected generateRepeatableTemplate(
type: string,
traderId: string,
side: string,
sessionId: string,
): IRepeatableQuest {
const questClone = this.cloner.clone<IRepeatableQuest>( const questClone = this.cloner.clone<IRepeatableQuest>(
this.databaseService.getTemplates().repeatableQuests.templates[type], this.databaseService.getTemplates().repeatableQuests.templates[type],
); );
@ -882,6 +903,10 @@ export class RepeatableQuestGenerator {
.replace("{traderId}", desiredTraderId) .replace("{traderId}", desiredTraderId)
.replace("{templateId}", questClone.templateId); .replace("{templateId}", questClone.templateId);
questClone.questStatus.id = this.objectId.generate();
questClone.questStatus.uid = sessionId; // Needs to match user id
questClone.questStatus.qid = questClone._id; // Needs to match quest id
return questClone; return questClone;
} }
} }

View File

@ -23,6 +23,7 @@ import { DatabaseService } from "@spt/services/DatabaseService";
import { ItemFilterService } from "@spt/services/ItemFilterService"; import { ItemFilterService } from "@spt/services/ItemFilterService";
import { LocalisationService } from "@spt/services/LocalisationService"; import { LocalisationService } from "@spt/services/LocalisationService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService"; import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { HashUtil } from "@spt/utils/HashUtil";
import { MathUtil } from "@spt/utils/MathUtil"; import { MathUtil } from "@spt/utils/MathUtil";
import { ObjectId } from "@spt/utils/ObjectId"; import { ObjectId } from "@spt/utils/ObjectId";
import { RandomUtil } from "@spt/utils/RandomUtil"; import { RandomUtil } from "@spt/utils/RandomUtil";
@ -36,6 +37,7 @@ export class RepeatableQuestRewardGenerator {
constructor( constructor(
@inject("PrimaryLogger") protected logger: ILogger, @inject("PrimaryLogger") protected logger: ILogger,
@inject("RandomUtil") protected randomUtil: RandomUtil, @inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("MathUtil") protected mathUtil: MathUtil, @inject("MathUtil") protected mathUtil: MathUtil,
@inject("DatabaseService") protected databaseService: DatabaseService, @inject("DatabaseService") protected databaseService: DatabaseService,
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@ -93,14 +95,18 @@ export class RepeatableQuestRewardGenerator {
const rewards: IQuestRewards = { Started: [], Success: [], Fail: [] }; const rewards: IQuestRewards = { Started: [], Success: [], Fail: [] };
// Start reward index to keep track // Start reward index to keep track
let rewardIndex = 0; let rewardIndex = -1;
// Add xp reward // Add xp reward
if (rewardParams.rewardXP > 0) { if (rewardParams.rewardXP > 0) {
rewards.Success.push({ rewards.Success.push({
id: this.hashUtil.generate(),
unknown: false,
gameMode: [],
availableInGameEditions: [],
index: rewardIndex,
value: rewardParams.rewardXP, value: rewardParams.rewardXP,
type: QuestRewardType.EXPERIENCE, type: QuestRewardType.EXPERIENCE,
index: rewardIndex,
}); });
rewardIndex++; rewardIndex++;
} }
@ -163,6 +169,10 @@ export class RepeatableQuestRewardGenerator {
// Add rep reward to rewards array // Add rep reward to rewards array
if (rewardParams.rewardReputation > 0) { if (rewardParams.rewardReputation > 0) {
const reward: IQuestReward = { const reward: IQuestReward = {
id: this.hashUtil.generate(),
unknown: false,
gameMode: [],
availableInGameEditions: [],
target: traderId, target: traderId,
value: rewardParams.rewardReputation, value: rewardParams.rewardReputation,
type: QuestRewardType.TRADER_STANDING, type: QuestRewardType.TRADER_STANDING,
@ -171,13 +181,17 @@ export class RepeatableQuestRewardGenerator {
rewards.Success.push(reward); rewards.Success.push(reward);
rewardIndex++; rewardIndex++;
this.logger.debug(` Adding ${rewardParams.rewardReputation} trader reputation reward`); this.logger.debug(`Adding: ${rewardParams.rewardReputation} ${traderId} trader reputation reward`);
} }
// Chance of adding skill reward // Chance of adding skill reward
if (this.randomUtil.getChance100(rewardParams.skillRewardChance * 100)) { if (this.randomUtil.getChance100(rewardParams.skillRewardChance * 100)) {
const targetSkill = this.randomUtil.getArrayValue(questConfig.possibleSkillRewards); const targetSkill = this.randomUtil.getArrayValue(questConfig.possibleSkillRewards);
const reward: IQuestReward = { const reward: IQuestReward = {
id: this.hashUtil.generate(),
unknown: false,
gameMode: [],
availableInGameEditions: [],
target: targetSkill, target: targetSkill,
value: rewardParams.skillPointReward, value: rewardParams.skillPointReward,
type: QuestRewardType.SKILL, type: QuestRewardType.SKILL,
@ -503,17 +517,23 @@ export class RepeatableQuestRewardGenerator {
* @param preset Optional array of preset items * @param preset Optional array of preset items
* @returns {object} Object of "Reward"-item-type * @returns {object} Object of "Reward"-item-type
*/ */
protected generateItemReward(tpl: string, count: number, index: number): IQuestReward { protected generateItemReward(tpl: string, count: number, index: number, foundInRaid = true): IQuestReward {
const id = this.objectId.generate(); const id = this.objectId.generate();
const questRewardItem: IQuestReward = { const questRewardItem: IQuestReward = {
id: this.hashUtil.generate(),
unknown: false,
gameMode: [],
availableInGameEditions: [],
index: index,
target: id, target: id,
value: count, value: count,
isEncoded: false,
findInRaid: foundInRaid,
type: QuestRewardType.ITEM, type: QuestRewardType.ITEM,
index: index,
items: [], items: [],
}; };
const rootItem = { _id: id, _tpl: tpl, upd: { StackObjectsCount: count, SpawnedInSession: true } }; const rootItem = { _id: id, _tpl: tpl, upd: { StackObjectsCount: count, SpawnedInSession: foundInRaid } };
questRewardItem.items = [rootItem]; questRewardItem.items = [rootItem];
return questRewardItem; return questRewardItem;
@ -528,13 +548,25 @@ export class RepeatableQuestRewardGenerator {
* @param preset Optional array of preset items * @param preset Optional array of preset items
* @returns {object} Object of "Reward"-item-type * @returns {object} Object of "Reward"-item-type
*/ */
protected generatePresetReward(tpl: string, count: number, index: number, preset?: IItem[]): IQuestReward { protected generatePresetReward(
tpl: string,
count: number,
index: number,
preset?: IItem[],
foundInRaid = true,
): IQuestReward {
const id = this.objectId.generate(); const id = this.objectId.generate();
const questRewardItem: IQuestReward = { const questRewardItem: IQuestReward = {
id: this.hashUtil.generate(),
unknown: false,
gameMode: [],
availableInGameEditions: [],
index: index,
target: id, target: id,
value: count, value: count,
isEncoded: false,
findInRaid: foundInRaid,
type: QuestRewardType.ITEM, type: QuestRewardType.ITEM,
index: index,
items: [], items: [],
}; };
@ -544,6 +576,10 @@ export class RepeatableQuestRewardGenerator {
this.logger.warning(`Root item of preset: ${tpl} not found`); this.logger.warning(`Root item of preset: ${tpl} not found`);
} }
if (rootItem.upd) {
rootItem.upd.SpawnedInSession = foundInRaid;
}
questRewardItem.items = this.itemHelper.reparentItemAndChildren(rootItem, preset); questRewardItem.items = this.itemHelper.reparentItemAndChildren(rootItem, preset);
questRewardItem.target = rootItem._id; // Target property and root items id must match questRewardItem.target = rootItem._id; // Target property and root items id must match
@ -641,6 +677,6 @@ export class RepeatableQuestRewardGenerator {
currency === Money.EUROS ? this.handbookHelper.fromRUB(rewardRoubles, Money.EUROS) : rewardRoubles; currency === Money.EUROS ? this.handbookHelper.fromRUB(rewardRoubles, Money.EUROS) : rewardRoubles;
// Get chosen currency + amount and return // Get chosen currency + amount and return
return this.generateItemReward(currency, rewardAmountToGivePlayer, rewardIndex); return this.generateItemReward(currency, rewardAmountToGivePlayer, rewardIndex, false);
} }
} }

View File

@ -160,8 +160,10 @@ export interface IQuestReward {
loyaltyLevel?: number; loyaltyLevel?: number;
/** Hideout area id */ /** Hideout area id */
traderId?: string; traderId?: string;
isEncoded?: boolean;
unknown?: boolean; unknown?: boolean;
findInRaid?: boolean; findInRaid?: boolean;
gameMode?: string[];
/** Game editions whitelisted to get reward */ /** Game editions whitelisted to get reward */
availableInGameEditions?: string[]; availableInGameEditions?: string[];
/** Game editions blacklisted from getting reward */ /** Game editions blacklisted from getting reward */

View File

@ -4,6 +4,12 @@ export interface IRepeatableQuest extends IQuest {
changeCost: IChangeCost[]; changeCost: IChangeCost[];
changeStandingCost: number; changeStandingCost: number;
sptRepatableGroupName: string; sptRepatableGroupName: string;
acceptanceAndFinishingSource: string;
progressSource: string;
rankingModes: string[];
gameModes: string[];
arenaLocations: string[];
questStatus: IRepeatableQuestStatus;
} }
export interface IRepeatableQuestDatabase { export interface IRepeatableQuestDatabase {
@ -13,6 +19,15 @@ export interface IRepeatableQuestDatabase {
samples: ISampleQuests[]; samples: ISampleQuests[];
} }
export interface IRepeatableQuestStatus {
id: string;
uid: string;
qid: string;
startTime: number;
status: number;
statusTimers: any;
}
export interface IRepeatableTemplates { export interface IRepeatableTemplates {
Elimination: IQuest; Elimination: IQuest;
Completion: IQuest; Completion: IQuest;