Updated server handling of quests/repeatable quests to match 0.14 changes - this will break profiles

This commit is contained in:
Dev 2024-01-05 19:52:21 +00:00
parent 3979e6ef61
commit 9dbd3d1acf
9 changed files with 316 additions and 572 deletions

View File

@ -19,39 +19,51 @@
"conditions": {
"AvailableForStart": [],
"AvailableForFinish": [{
"_props": {
"id": "618c1de4d4cd91439f3de4ae",
"parentId": "",
"dynamicLocale": true,
"index": 0,
"visibilityConditions": [],
"value": 1,
"type": "Elimination",
"oneSessionOnly": false,
"doNotResetIfCounterCompleted": false,
"counter": {
"id": "618c1de4d4cd91439f3de4ac",
"conditions": [{
"_props": {
"target": "Savage",
"value": 1,
"savageRole": [
"bossBully"
],
"id": "618c1de4d4cd91439f3de4ad",
"dynamicLocale": true
},
"_parent": "Kills"
}
]
}
},
"_parent": "CounterCreator",
"dynamicLocale": true
}
],
"id": "618c1de4d4cd91439f3de4ae",
"index": 0,
"dynamicLocale": true,
"visibilityConditions": [],
"globalQuestCounterId": "",
"parentId": "",
"value": 1,
"type": "Elimination",
"oneSessionOnly": false,
"doNotResetIfCounterCompleted": false,
"counter": {
"id": "618c1de4d4cd91439f3de4ac",
"conditions": [{
"id": "618c1de4d4cd91439f3de4ad",
"dynamicLocale": true,
"target": "Savage",
"compareMethod": ">=",
"value": 1,
"weapon": [],
"distance": {
"value": 0,
"compareMethod": ">="
},
"weaponModsInclusive": [],
"weaponModsExclusive": [],
"enemyEquipmentInclusive": [],
"enemyEquipmentExclusive": [],
"weaponCaliber": [],
"savageRole": [],
"bodyPart": [],
"daytime": {
"from": 0,
"to": 0
},
"enemyHealthEffects": [],
"resetOnSessionEnd": false,
"conditionType": "Kills"
}
]
},
"conditionType": "CounterCreator"
}],
"Fail": []
},
"side": "Pmc",
"name": "{templateId} name {traderId}",
"note": "{templateId} note {traderId}",
"description": "{templateId} description {traderId} 0",
@ -91,6 +103,7 @@
"AvailableForFinish": [],
"Fail": []
},
"side": "Pmc",
"name": "{templateId} name {traderId}",
"note": "{templateId} note {traderId}",
"description": "{templateId} description {traderId} 0",
@ -110,7 +123,7 @@
"changeStandingCost": 0
},
"Exploration": {
"_id": null,
"_id": "65947c6afb90e7fcb40f8d684",
"traderId": "54cb50c76803fa8b248b4571",
"location": null,
"image": "/files/quest/icon/616d993bc8c5ad2ab30ff6ba.jpg",
@ -128,44 +141,40 @@
"conditions": {
"AvailableForStart": [],
"AvailableForFinish": [{
"_props": {
"id": "618c1de4d4cd91439f3de4a5",
"parentId": "",
"dynamicLocale": true,
"index": 0,
"visibilityConditions": [],
"value": 1,
"type": "Completion",
"oneSessionOnly": false,
"doNotResetIfCounterCompleted": false,
"counter": {
"id": "618c1de4d4cd91439f3de4a4",
"conditions": [{
"_props": {
"status": [
"Survived"
],
"id": "618c1de4d4cd91439f3de4a3",
"dynamicLocale": true
},
"_parent": "ExitStatus"
}, {
"_props": {
"target": [],
"id": "618c1de4d4cd91439f3de4a2",
"dynamicLocale": true
},
"_parent": "Location"
}
]
}
},
"_parent": "CounterCreator",
"dynamicLocale": true
"id": "618c1de4d4cd91439f3de4a5",
"index": 0,
"dynamicLocale": true,
"visibilityConditions": [],
"globalQuestCounterId": "",
"parentId": "",
"value": 1,
"type": "Completion",
"oneSessionOnly": false,
"completeInSeconds": 0,
"doNotResetIfCounterCompleted": false,
"counter": {
"id": "618c1de4d4cd91439f3de4a4",
"conditions": [{
"status": [
"Survived"
],
"id": "618c1de4d4cd91439f3de4a3",
"dynamicLocale": true,
"conditionType": "ExitStatus"
}, {
"target": [],
"id": "618c1de4d4cd91439f3de4a2",
"dynamicLocale": true,
"conditionType": "Location"
}
]
},
"conditionType": "CounterCreator"
}
],
"Fail": []
},
"side": "Pmc",
"name": "{templateId} name {traderId}",
"note": "{templateId} note {traderId}",
"description": "{templateId} description {traderId} 0",
@ -203,73 +212,60 @@
"conditions": {
"AvailableForStart": [],
"AvailableForFinish": [{
"_props": {
"id": "64cfb3818db9f48b3f0b0a6f",
"parentId": "",
"dynamicLocale": false,
"index": 0,
"visibilityConditions": [],
"globalQuestCounterId": null,
"target": ["5b47574386f77428ca22b336"],
"value": 7,
"minDurability": 0,
"maxDurability": 100,
"dogtagLevel": 0,
"onlyFoundInRaid": false,
"isEncoded": false,
"countInRaid": true
},
"_parent": "FindItem",
"dynamicLocale": false
"id": "64cfb3818db9f48b3f0b0a6f",
"parentId": "",
"dynamicLocale": false,
"index": 0,
"visibilityConditions": [],
"globalQuestCounterId": null,
"target": ["5b47574386f77428ca22b336"],
"value": 7,
"minDurability": 0,
"maxDurability": 100,
"dogtagLevel": 0,
"onlyFoundInRaid": false,
"isEncoded": false,
"countInRaid": true,
"conditionType": "FindItem"
}, {
"_props": {
"id": "64cfb3818db9f48b3f0b0a74",
"parentId": "",
"dynamicLocale": true,
"index": 0,
"visibilityConditions": [],
"globalQuestCounterId": null,
"value": 1,
"type": "PickUp",
"completeInSeconds": 0,
"oneSessionOnly": true,
"doNotResetIfCounterCompleted": false,
"counter": {
"id": "64cfb3818db9f48b3f0b0a73",
"conditions": [{
"_props": {
"target": ["any"],
"id": "64cfb3818db9f48b3f0b0a70",
"dynamicLocale": true
},
"_parent": "Location"
}, {
"_props": {
"status": ["Survived"],
"id": "64cfb3818db9f48b3f0b0a71",
"dynamicLocale": true
},
"_parent": "ExitStatus"
}, {
"_props": {
"equipmentInclusive": [["5b47574386f77428ca22b336"]],
"IncludeNotEquippedItems": true,
"id": "64cfb3818db9f48b3f0b0a72",
"dynamicLocale": true
},
"_parent": "Equipment"
}
]
}
},
"_parent": "CounterCreator",
"dynamicLocale": true
"id": "64cfb3818db9f48b3f0b0a74",
"parentId": "",
"dynamicLocale": true,
"index": 0,
"visibilityConditions": [],
"globalQuestCounterId": null,
"value": 1,
"type": "PickUp",
"completeInSeconds": 0,
"oneSessionOnly": true,
"doNotResetIfCounterCompleted": false,
"counter": {
"id": "64cfb3818db9f48b3f0b0a73",
"conditions": [{
"target": ["any"],
"id": "64cfb3818db9f48b3f0b0a70",
"dynamicLocale": true,
"conditionType": "Location"
}, {
"status": ["Survived"],
"id": "64cfb3818db9f48b3f0b0a71",
"dynamicLocale": true,
"conditionType": "ExitStatus"
}, {
"equipmentInclusive": [["5b47574386f77428ca22b336"]],
"IncludeNotEquippedItems": true,
"id": "64cfb3818db9f48b3f0b0a72",
"dynamicLocale": true,
"conditionType": "Equipment"
}
]
},
"conditionType": "CounterCreator"
}
],
"Fail": []
},
"side": "Scav",
"questStatus": {},
"name": "{templateId} name {traderId}",
"note": "{templateId} note {traderId}",
"description": "{templateId} description {traderId} 0",

View File

@ -9,7 +9,7 @@ import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { IQuestStatus } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { AvailableForConditions, IQuest, Reward } from "@spt-aki/models/eft/common/tables/IQuest";
import { IQuest, IQuestCondition } from "@spt-aki/models/eft/common/tables/IQuest";
import { IPmcDataRepeatableQuest, IRepeatableQuest } from "@spt-aki/models/eft/common/tables/IRepeatableQuests";
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse";
import { IAcceptQuestRequestData } from "@spt-aki/models/eft/quests/IAcceptQuestRequestData";
@ -135,7 +135,7 @@ export class QuestController
for (const conditionToFulfil of questRequirements)
{
// If the previous quest isn't in the user profile, it hasn't been completed or started
const prerequisiteQuest = profile.Quests.find((pq) => pq.qid === conditionToFulfil.target);
const prerequisiteQuest = profile.Quests.find((profileQuest) => conditionToFulfil.target.includes(profileQuest.qid));
if (!prerequisiteQuest)
{
haveCompletedPreviousQuest = false;
@ -609,7 +609,7 @@ export class QuestController
sessionID: string,
pmcData: IPmcData,
completedQuestId: string,
questRewards: Reward[],
questRewards: Item[],
): void
{
const quest = this.questHelper.getQuestFromDb(completedQuestId, pmcData);
@ -637,7 +637,7 @@ export class QuestController
{
// If quest has prereq of completed quest + availableAfter value > 0 (quest has wait time)
const nextQuestWaitCondition = quest.conditions.AvailableForStart.find((x) =>
x.target === completedQuestId && x.availableAfter > 0
x.target.includes(completedQuestId) && x.availableAfter > 0
);
if (nextQuestWaitCondition)
{
@ -687,7 +687,7 @@ export class QuestController
return false;
}
return x.conditions.Fail.some((y) => y.target === completedQuestId);
return x.conditions.Fail.some((y) => y.target.includes(completedQuestId));
});
}
@ -760,7 +760,7 @@ export class QuestController
let handedInCount = 0;
// Decrement number of items handed in
let handoverRequirements: AvailableForConditions;
let handoverRequirements: IQuestCondition;
for (const condition of quest.conditions.AvailableForFinish)
{
if (
@ -897,7 +897,7 @@ export class QuestController
protected showQuestItemHandoverMatchError(
handoverQuestRequest: IHandoverQuestRequestData,
itemHandedOver: Item,
handoverRequirements: AvailableForConditions,
handoverRequirements: IQuestCondition,
output: IItemEventRouterResponse,
): IItemEventRouterResponse
{

View File

@ -182,7 +182,7 @@ export class RepeatableQuestController
}
}
// create stupid redundant change requirements from quest data
// Create stupid redundant change requirements from quest data
for (const quest of currentRepeatableQuestType.activeQuests)
{
currentRepeatableQuestType.changeRequirement[quest._id] = {

View File

@ -9,24 +9,13 @@ import { RepeatableQuestHelper } from "@spt-aki/helpers/RepeatableQuestHelper";
import { Exit, ILocationBase } from "@spt-aki/models/eft/common/ILocationBase";
import { TraderInfo } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import {
ICompletion,
ICompletionAvailableFor,
IElimination,
IEliminationCondition,
IEquipmentConditionProps,
IExploration,
IExplorationCondition,
IKillConditionProps,
IPickup,
IRepeatableQuest,
IReward,
IRewards,
} from "@spt-aki/models/eft/common/tables/IRepeatableQuests";
import { IQuestCondition, IQuestConditionCounterCondition, IQuestReward, IQuestRewards } from "@spt-aki/models/eft/common/tables/IQuest";
import { IRepeatableQuest } from "@spt-aki/models/eft/common/tables/IRepeatableQuests";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Money } from "@spt-aki/models/enums/Money";
import { QuestRewardType } from "@spt-aki/models/enums/QuestRewardType";
import { Traders } from "@spt-aki/models/enums/Traders";
import {
IBaseQuestConfig,
@ -136,7 +125,7 @@ export class RepeatableQuestGenerator
traderId: string,
questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig,
): IElimination
): IRepeatableQuest
{
const eliminationConfig = this.repeatableQuestHelper.getEliminationConfigByPmcLevel(pmcLevel, repeatableConfig);
const locationsConfig = repeatableConfig.locations;
@ -337,7 +326,7 @@ export class RepeatableQuestGenerator
// 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 quest = this.generateRepeatableTemplate("Elimination", traderId, repeatableConfig.side) as IElimination;
const quest = this.generateRepeatableTemplate("Elimination", traderId, repeatableConfig.side);
// ASSUMPTION: All fence quests are for scavs
if (traderId === Traders.FENCE)
@ -346,17 +335,17 @@ export class RepeatableQuestGenerator
}
const availableForFinishCondition = quest.conditions.AvailableForFinish[0];
availableForFinishCondition._props.counter.id = this.objectId.generate();
availableForFinishCondition._props.counter.conditions = [];
availableForFinishCondition.counter.id = this.objectId.generate();
availableForFinishCondition.counter.conditions = [];
// Only add specific location condition if specific map selected
if (locationKey !== "any")
{
availableForFinishCondition._props.counter.conditions.push(
availableForFinishCondition.counter.conditions.push(
this.generateEliminationLocation(locationsConfig[locationKey]),
);
}
availableForFinishCondition._props.counter.conditions.push(
availableForFinishCondition.counter.conditions.push(
this.generateEliminationCondition(
targetKey,
bodyPartsToClient,
@ -365,8 +354,8 @@ export class RepeatableQuestGenerator
allowedWeaponsCategory,
),
);
availableForFinishCondition._props.value = desiredKillCount;
availableForFinishCondition._props.id = this.objectId.generate();
availableForFinishCondition.value = desiredKillCount;
availableForFinishCondition.id = this.objectId.generate();
quest.location = this.getQuestLocationByMapId(locationKey);
quest.rewards = this.generateReward(
@ -413,11 +402,13 @@ export class RepeatableQuestGenerator
* @param {string} location the location on which to fulfill the elimination quest
* @returns {IEliminationCondition} object of "Elimination"-location-subcondition
*/
protected generateEliminationLocation(location: string[]): IEliminationCondition
protected generateEliminationLocation(location: string[]): IQuestConditionCounterCondition
{
const propsObject: IEliminationCondition = {
_props: { target: location, id: this.objectId.generate(), dynamicLocale: true },
_parent: "Location",
const propsObject: IQuestConditionCounterCondition = {
id: this.objectId.generate(),
dynamicLocale: true,
target: location,
conditionType: "Location"
};
return propsObject;
@ -438,13 +429,14 @@ export class RepeatableQuestGenerator
distance: number,
allowedWeapon: string,
allowedWeaponCategory: string,
): IEliminationCondition
): IQuestConditionCounterCondition
{
const killConditionProps: IKillConditionProps = {
const killConditionProps: IQuestConditionCounterCondition = {
target: target,
value: 1,
id: this.objectId.generate(),
dynamicLocale: true,
conditionType: "Kills",
};
if (target.startsWith("boss"))
@ -474,10 +466,11 @@ export class RepeatableQuestGenerator
// Has specific weapon category requirement
if (allowedWeaponCategory?.length > 0)
{
killConditionProps.weaponCategories = [allowedWeaponCategory];
// TODO - fix - does weaponCategories exist?
//killConditionProps.weaponCategories = [allowedWeaponCategory];
}
return { _props: killConditionProps, _parent: "Kills" };
return killConditionProps;
}
/**
@ -492,7 +485,7 @@ export class RepeatableQuestGenerator
pmcLevel: number,
traderId: string,
repeatableConfig: IRepeatableQuestConfig,
): ICompletion
): IRepeatableQuest
{
const completionConfig = repeatableConfig.questConfig.Completion;
const levelsConfig = repeatableConfig.rewardScaling.levels;
@ -500,7 +493,7 @@ export class RepeatableQuestGenerator
const distinctItemsToRetrieveCount = this.randomUtil.getInt(1, completionConfig.uniqueItemCount);
const quest = this.generateRepeatableTemplate("Completion", traderId, repeatableConfig.side) as ICompletion;
const quest = this.generateRepeatableTemplate("Completion", traderId, repeatableConfig.side);
// 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.getRewardableItems(repeatableConfig, traderId);
@ -628,7 +621,7 @@ export class RepeatableQuestGenerator
* @param {integer} value amount of items of this specific type to request
* @returns {object} object of "Completion"-condition
*/
protected generateCompletionAvailableForFinish(itemTpl: string, value: number): ICompletionAvailableFor
protected generateCompletionAvailableForFinish(itemTpl: string, value: number): IQuestCondition
{
let minDurability = 0;
let onlyFoundInRaid = true;
@ -650,21 +643,18 @@ export class RepeatableQuestGenerator
}
return {
_props: {
id: this.objectId.generate(),
parentId: "",
dynamicLocale: true,
index: 0,
visibilityConditions: [],
target: [itemTpl],
value: value,
minDurability: minDurability,
maxDurability: 100,
dogtagLevel: 0,
onlyFoundInRaid: onlyFoundInRaid,
},
_parent: "HandoverItem",
id: this.objectId.generate(),
parentId: "",
dynamicLocale: true,
index: 0,
visibilityConditions: [],
target: [itemTpl],
value: value,
minDurability: minDurability,
maxDurability: 100,
dogtagLevel: 0,
onlyFoundInRaid: onlyFoundInRaid,
conditionType: "HandoverItem",
};
}
@ -682,7 +672,7 @@ export class RepeatableQuestGenerator
traderId: string,
questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig,
): IExploration
): IRepeatableQuest
{
const explorationConfig = repeatableConfig.questConfig.Exploration;
const requiresSpecificExtract = Math.random() < repeatableConfig.questConfig.Exploration.specificExits.probability;
@ -705,24 +695,27 @@ export class RepeatableQuestGenerator
// Different max extract count when specific extract needed
const numExtracts = this.randomUtil.randInt(1, requiresSpecificExtract ? explorationConfig.maxExtractsWithSpecificExit : explorationConfig.maxExtracts + 1);
const quest = this.generateRepeatableTemplate("Exploration", traderId, repeatableConfig.side) as IExploration;
const quest = this.generateRepeatableTemplate("Exploration", traderId, repeatableConfig.side);
const exitStatusCondition: IExplorationCondition = {
_parent: "ExitStatus",
_props: { id: this.objectId.generate(), dynamicLocale: true, status: ["Survived"] },
const exitStatusCondition: IQuestConditionCounterCondition = {
conditionType: "ExitStatus",
id: this.objectId.generate(),
dynamicLocale: true,
status: ["Survived"],
};
const locationCondition: IExplorationCondition = {
_parent: "Location",
_props: { id: this.objectId.generate(), dynamicLocale: true, target: locationTarget },
const locationCondition: IQuestConditionCounterCondition = {
conditionType: "Location",
id: this.objectId.generate(),
dynamicLocale: true,
target: locationTarget,
};
quest.conditions.AvailableForFinish[0]._props.counter.id = this.objectId.generate();
quest.conditions.AvailableForFinish[0]._props.counter.conditions = [exitStatusCondition, locationCondition];
quest.conditions.AvailableForFinish[0]._props.value = numExtracts;
quest.conditions.AvailableForFinish[0]._props.id = this.objectId.generate();
quest.conditions.AvailableForFinish[0].counter.id = this.objectId.generate();
quest.conditions.AvailableForFinish[0].counter.conditions = [exitStatusCondition, locationCondition];
quest.conditions.AvailableForFinish[0].value = numExtracts;
quest.conditions.AvailableForFinish[0].id = this.objectId.generate();
quest.location = this.getQuestLocationByMapId(locationKey);
if (requiresSpecificExtract)
{
// Filter by whitelist, it's also possible that the field "PassageRequirement" does not exist (e.g. Shoreline)
@ -737,7 +730,7 @@ export class RepeatableQuestGenerator
);
const exit = this.randomUtil.drawRandomFromList(possibleExists, 1)[0];
const exitCondition = this.generateExplorationExitCondition(exit);
quest.conditions.AvailableForFinish[0]._props.counter.conditions.push(exitCondition);
quest.conditions.AvailableForFinish[0].counter.conditions.push(exitCondition);
}
// Difficulty for exploration goes from 1 extract to maxExtracts
@ -753,11 +746,11 @@ export class RepeatableQuestGenerator
traderId: string,
questTypePool: IQuestTypePool,
repeatableConfig: IRepeatableQuestConfig,
): IPickup
): IRepeatableQuest
{
const pickupConfig = repeatableConfig.questConfig.Pickup;
const quest = this.generateRepeatableTemplate("Pickup", traderId, repeatableConfig.side) as IPickup;
const quest = this.generateRepeatableTemplate("Pickup", traderId, repeatableConfig.side);
const itemTypeToFetchWithCount = this.randomUtil.getArrayValue(pickupConfig.ItemTypeToFetchWithMaxCount);
const itemCountToFetch = this.randomUtil.randInt(
@ -768,18 +761,18 @@ export class RepeatableQuestGenerator
// const locationKey: string = this.randomUtil.drawRandomFromDict(questTypePool.pool.Pickup.locations)[0];
// const locationTarget = questTypePool.pool.Pickup.locations[locationKey];
const findCondition = quest.conditions.AvailableForFinish.find((x) => x._parent === "FindItem");
findCondition._props.target = [itemTypeToFetchWithCount.itemType];
findCondition._props.value = itemCountToFetch;
const findCondition = quest.conditions.AvailableForFinish.find((x) => x.conditionType === "FindItem");
findCondition.target = [itemTypeToFetchWithCount.itemType];
findCondition.value = itemCountToFetch;
const counterCreatorCondition = quest.conditions.AvailableForFinish.find((x) => x._parent === "CounterCreator");
const counterCreatorCondition = quest.conditions.AvailableForFinish.find((x) => x.conditionType === "CounterCreator");
// const locationCondition = counterCreatorCondition._props.counter.conditions.find(x => x._parent === "Location");
// (locationCondition._props as ILocationConditionProps).target = [...locationTarget];
const equipmentCondition = counterCreatorCondition._props.counter.conditions.find((x) =>
x._parent === "Equipment"
const equipmentCondition = counterCreatorCondition.counter.conditions.find((x) =>
x.conditionType === "Equipment"
);
(equipmentCondition._props as IEquipmentConditionProps).equipmentInclusive = [[
equipmentCondition.equipmentInclusive = [[
itemTypeToFetchWithCount.itemType,
]];
@ -806,11 +799,13 @@ export class RepeatableQuestGenerator
* @param {string} exit The exit name to generate the condition for
* @returns {object} Exit condition
*/
protected generateExplorationExitCondition(exit: Exit): IExplorationCondition
protected generateExplorationExitCondition(exit: Exit): IQuestConditionCounterCondition
{
return {
_parent: "ExitName",
_props: { exitName: exit.Name, id: this.objectId.generate(), dynamicLocale: true },
conditionType: "ExitName",
exitName: exit.Name,
id: this.objectId.generate(),
dynamicLocale: true,
};
}
@ -840,7 +835,7 @@ export class RepeatableQuestGenerator
traderId: string,
repeatableConfig: IRepeatableQuestConfig,
questConfig: IBaseQuestConfig,
): IRewards
): IQuestRewards
{
// difficulty could go from 0.2 ... -> for lowest diffuculty receive 0.2*nominal reward
const levelsConfig = repeatableConfig.rewardScaling.levels;
@ -883,7 +878,7 @@ export class RepeatableQuestGenerator
let roublesBudget = rewardRoubles;
let rewardItemPool = this.chooseRewardItemsWithinBudget(repeatableConfig, roublesBudget, traderId);
const rewards: IRewards = {
const rewards: IQuestRewards = {
Started: [],
Success: [],
Fail: [],
@ -893,7 +888,7 @@ export class RepeatableQuestGenerator
// Add xp reward
if (rewardXP > 0)
{
rewards.Success.push({ value: rewardXP, type: "Experience", index: rewardIndex });
rewards.Success.push({ value: rewardXP, type: QuestRewardType.EXPERIENCE, index: rewardIndex });
rewardIndex++;
}
@ -987,7 +982,11 @@ export class RepeatableQuestGenerator
// Add rep reward to rewards array
if (rewardReputation > 0)
{
const reward: IReward = { target: traderId, value: rewardReputation, type: "TraderStanding", index: rewardIndex };
const reward: IQuestReward = {
target: traderId,
value: rewardReputation,
type: QuestRewardType.TRADER_STANDING,
index: rewardIndex };
rewards.Success.push(reward);
rewardIndex++;
}
@ -995,10 +994,10 @@ export class RepeatableQuestGenerator
// Chance of adding skill reward
if (this.randomUtil.getChance100(skillRewardChance * 100))
{
const reward: IReward = {
const reward: IQuestReward = {
target: this.randomUtil.getArrayValue(questConfig.possibleSkillRewards),
value: skillPointReward,
type: "Skill",
type: QuestRewardType.SKILL,
index: rewardIndex,
};
rewards.Success.push(reward);
@ -1028,10 +1027,13 @@ export class RepeatableQuestGenerator
protected getRandomisedRewardItemStackSizeByPrice(item: ITemplateItem): number
{
const rewardItemPrice = this.itemHelper.getStaticItemPrice(item._id);
if (rewardItemPrice < 3000) {
if (rewardItemPrice < 3000)
{
return this.randomUtil.getArrayValue([2, 3, 4]);
}
else if (rewardItemPrice < 10000) {
if (rewardItemPrice < 10000)
{
return this.randomUtil.getArrayValue([2, 3]);
}
@ -1082,10 +1084,10 @@ export class RepeatableQuestGenerator
* @param {integer} index All rewards will be appended to a list, for unknown reasons the client wants the index
* @returns {object} Object of "Reward"-item-type
*/
protected generateRewardItem(tpl: string, value: number, index: number, preset: Item[] = null): IReward
protected generateRewardItem(tpl: string, value: number, index: number, preset: Item[] = null): IQuestReward
{
const id = this.objectId.generate();
const rewardItem: IReward = { target: id, value: value, type: "Item", index: index };
const rewardItem: IQuestReward = { target: id, value: value, type: QuestRewardType.ITEM, index: index };
if (preset)
{

View File

@ -1,47 +1,47 @@
import { injectable } from "tsyringe";
import { AvailableForConditions } from "@spt-aki/models/eft/common/tables/IQuest";
import { IQuestCondition } from "@spt-aki/models/eft/common/tables/IQuest";
@injectable()
export class QuestConditionHelper
{
public getQuestConditions(
q: AvailableForConditions[],
furtherFilter: (a: AvailableForConditions) => AvailableForConditions[] = null,
): AvailableForConditions[]
q: IQuestCondition[],
furtherFilter: (a: IQuestCondition) => IQuestCondition[] = null,
): IQuestCondition[]
{
return this.filterConditions(q, "Quest", furtherFilter);
}
public getLevelConditions(
q: AvailableForConditions[],
furtherFilter: (a: AvailableForConditions) => AvailableForConditions[] = null,
): AvailableForConditions[]
q: IQuestCondition[],
furtherFilter: (a: IQuestCondition) => IQuestCondition[] = null,
): IQuestCondition[]
{
return this.filterConditions(q, "Level", furtherFilter);
}
public getLoyaltyConditions(
q: AvailableForConditions[],
furtherFilter: (a: AvailableForConditions) => AvailableForConditions[] = null,
): AvailableForConditions[]
q: IQuestCondition[],
furtherFilter: (a: IQuestCondition) => IQuestCondition[] = null,
): IQuestCondition[]
{
return this.filterConditions(q, "TraderLoyalty", furtherFilter);
}
public getStandingConditions(
q: AvailableForConditions[],
furtherFilter: (a: AvailableForConditions) => AvailableForConditions[] = null,
): AvailableForConditions[]
q: IQuestCondition[],
furtherFilter: (a: IQuestCondition) => IQuestCondition[] = null,
): IQuestCondition[]
{
return this.filterConditions(q, "TraderStanding", furtherFilter);
}
protected filterConditions(
q: AvailableForConditions[],
q: IQuestCondition[],
questType: string,
furtherFilter: (a: AvailableForConditions) => AvailableForConditions[] = null,
): AvailableForConditions[]
furtherFilter: (a: IQuestCondition) => IQuestCondition[] = null,
): IQuestCondition[]
{
const filteredQuests = q.filter((c) =>
{

View File

@ -10,7 +10,7 @@ import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { Common, IQuestStatus } from "@spt-aki/models/eft/common/tables/IBotBase";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { AvailableForConditions, IQuest, Reward } from "@spt-aki/models/eft/common/tables/IQuest";
import { IQuest, IQuestCondition, IQuestReward } from "@spt-aki/models/eft/common/tables/IQuest";
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse";
import { IAcceptQuestRequestData } from "@spt-aki/models/eft/quests/IAcceptQuestRequestData";
import { IFailQuestRequestData } from "@spt-aki/models/eft/quests/IFailQuestRequestData";
@ -78,7 +78,7 @@ export class QuestHelper
* @param condition Quest condition
* @returns true if player level is greater than or equal to quest
*/
public doesPlayerLevelFulfilCondition(playerLevel: number, condition: AvailableForConditions): boolean
public doesPlayerLevelFulfilCondition(playerLevel: number, condition: IQuestCondition): boolean
{
if (condition.conditionType === "Level")
{
@ -198,7 +198,7 @@ export class QuestHelper
* @param profile Player profile
* @returns true if loyalty is high enough to fulfill quest requirement
*/
public traderLoyaltyLevelRequirementCheck(questProperties: AvailableForConditions, profile: IPmcData): boolean
public traderLoyaltyLevelRequirementCheck(questProperties: IQuestCondition, profile: IPmcData): boolean
{
const requiredLoyaltyLevel = Number(questProperties.value);
const trader = profile.TradersInfo[<string>questProperties.target];
@ -216,7 +216,7 @@ export class QuestHelper
* @param profile Player profile
* @returns true if standing is high enough to fulfill quest requirement
*/
public traderStandingRequirementCheck(questProperties: AvailableForConditions, profile: IPmcData): boolean
public traderStandingRequirementCheck(questProperties: IQuestCondition, profile: IPmcData): boolean
{
const requiredStanding = Number(questProperties.value);
const trader = profile.TradersInfo[<string>questProperties.target];
@ -253,17 +253,17 @@ export class QuestHelper
}
/**
* take reward item from quest and set FiR status + fix stack sizes + fix mod Ids
* @param reward Reward item to fix
* Take reward item from quest and set FiR status + fix stack sizes + fix mod Ids
* @param questReward Reward item to fix
* @returns Fixed rewards
*/
protected processReward(reward: Reward): Reward[]
protected processReward(questReward: IQuestReward): Item[]
{
let rewardItems: Reward[] = [];
let rewardItems: Item[] = [];
let targets: Item[] = [];
const mods: Item[] = [];
for (const item of reward.items)
for (const item of questReward.items)
{
// reward items are granted Found in Raid status
if (!item.upd)
@ -274,7 +274,7 @@ export class QuestHelper
item.upd.SpawnedInSession = true;
// separate base item and mods, fix stacks
if (item._id === reward.target)
if (item._id === questReward.target)
{
if (
(item.parentId !== undefined) && (item.parentId === "hideout")
@ -311,7 +311,7 @@ export class QuestHelper
items.push(this.jsonUtil.clone(mod));
}
rewardItems = rewardItems.concat(<Reward[]>this.ragfairServerHelper.reparentPresets(target, items));
rewardItems = rewardItems.concat(this.ragfairServerHelper.reparentPresets(target, items));
}
return rewardItems;
@ -323,11 +323,13 @@ export class QuestHelper
* @param status Quest status that holds the items (Started, Success, Fail)
* @returns array of items with the correct maxStack
*/
public getQuestRewardItems(quest: IQuest, status: QuestStatus): Reward[]
public getQuestRewardItems(quest: IQuest, status: QuestStatus): Item[]
{
// 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) : []
const questRewards = quest.rewards[QuestStatus[status]].flatMap((reward: IQuestReward) =>
reward.type === "Item"
? this.processReward(reward)
: []
);
return questRewards;
@ -410,7 +412,7 @@ export class QuestHelper
const acceptedQuestCondition = quest.conditions.AvailableForStart.find((x) =>
{
return x.conditionType === "Quest"
&& x.target === startedQuestId
&& x.target.includes(startedQuestId)
&& x.status[0] === QuestStatus.Started;
});
@ -466,7 +468,7 @@ export class QuestHelper
const acceptedQuestCondition = q.conditions.AvailableForStart.find((c) =>
{
return c.conditionType === "Quest"
&& c.target === failedQuestId
&& c.target.includes(failedQuestId)
&& c.status[0] === QuestStatus.Fail;
});
@ -495,7 +497,7 @@ export class QuestHelper
*/
public applyMoneyBoost(quest: IQuest, multiplier: number, questStatus: QuestStatus): IQuest
{
const rewards: Reward[] = quest.rewards?.[QuestStatus[questStatus]] ?? [];
const rewards: IQuestReward[] = quest.rewards?.[QuestStatus[questStatus]] ?? [];
for (const reward of rewards)
{
if (reward.type === "Item")
@ -786,7 +788,7 @@ export class QuestHelper
state: QuestStatus,
sessionId: string,
questResponse: IItemEventRouterResponse,
): Reward[]
): Item[]
{
// Repeatable quest base data is always in PMCProfile, `profileData` may be scav profile
// TODO: consider moving repeatable quest data to profile-agnostic location
@ -809,7 +811,7 @@ export class QuestHelper
// e.g. 'Success' or 'AvailableForFinish'
const questStateAsString = QuestStatus[state];
for (const reward of <Reward[]>questDetails.rewards[questStateAsString])
for (const reward of <IQuestReward[]>questDetails.rewards[questStateAsString])
{
switch (reward.type)
{
@ -873,7 +875,7 @@ export class QuestHelper
*/
protected findAndAddHideoutProductionIdToProfile(
pmcData: IPmcData,
craftUnlockReward: Reward,
craftUnlockReward: IQuestReward,
questDetails: IQuest,
sessionID: string,
response: IItemEventRouterResponse,

View File

@ -1,12 +1,12 @@
import { IQuestConditions, IRewards } from "./IQuest"
import { IQuestConditionTypes, IQuestRewards } from "./IQuest"
export interface IAchievement
{
id: string
imageUrl: string
assetPath: string
rewards: IRewards
conditions: IQuestConditions
rewards: IQuestRewards
conditions: IQuestConditionTypes
instantComplete: boolean
showNotificationsInGame: boolean
showProgress: boolean

View File

@ -9,7 +9,7 @@ export interface IQuest
QuestName?: string;
_id: string;
canShowNotificationsInGame: boolean;
conditions: IQuestConditions;
conditions: IQuestConditionTypes;
description: string;
failMessageText: string;
name: string;
@ -26,8 +26,11 @@ export interface IQuest
secretQuest: boolean;
startedMessageText: string;
successMessageText: string;
acceptPlayerMessage: string;
declinePlayerMessage: string;
completePlayerMessage: string;
templateId: string;
rewards: IRewards;
rewards: IQuestRewards;
/** Becomes 'AppearStatus' inside client */
status: string | number;
KeyQuest: boolean;
@ -38,26 +41,27 @@ export interface IQuest
sptStatus?: QuestStatus;
}
export interface IQuestConditions
export interface IQuestConditionTypes
{
Started: AvailableForConditions[];
AvailableForFinish: AvailableForConditions[];
AvailableForStart: AvailableForConditions[];
Success: AvailableForConditions[];
Fail: AvailableForConditions[];
Started: IQuestCondition[];
AvailableForFinish: IQuestCondition[];
AvailableForStart: IQuestCondition[];
Success: IQuestCondition[];
Fail: IQuestCondition[];
}
export interface AvailableForConditions
export interface IQuestCondition
{
id: string;
index: number;
parentId: string;
isEncoded: boolean;
index?: number;
compareMethod?: string
dynamicLocale: boolean;
value?: string | number;
compareMethod?: string;
visibilityConditions?: VisibilityCondition[];
target?: string | string[]; // TODO: split each availableForX object into each type: FindItem, HandoverItem, Level, Quest, TraderLoyalty etc
globalQuestCounterId?: string
parentId?: string;
target: string[] | string;
value?: string | number;
type?: boolean;
status?: QuestStatus[];
availableAfter?: number;
dispersion?: number;
@ -67,42 +71,42 @@ export interface AvailableForConditions
dogtagLevel?: number;
maxDurability?: number;
minDurability?: number;
counter?: AvailableForCounter;
counter?: IQuestConditionCounter;
plantTime?: number;
zoneId?: string;
type?: boolean;
countInRaid?: boolean;
globalQuestCounterId?: string;
completeInSeconds?: number
conditionType?: string
isEncoded?: boolean;
conditionType?: string;
}
export interface AvailableForCounter
export interface IQuestConditionCounter
{
id: string;
conditions: CounterCondition[];
conditions: IQuestConditionCounterCondition[];
}
export interface CounterCondition
export interface IQuestConditionCounterCondition
{
id: string;
completeInSeconds: number
dynamicLocale: boolean
target?: string[] | string; // TODO: some objects have an array and some are just strings, thanks bsg very cool
completeInSeconds?: number
energy?: IValueCompare
exitName?: string;
hydration?: IValueCompare
time?: IValueCompare
target: string[] | string; // TODO: some objects have an array and some are just strings, thanks bsg very cool
compareMethod?: string;
value?: string;
value?: number;
weapon?: string[];
distance: ICounterConditionDistance
distance?: ICounterConditionDistance
equipmentInclusive?: string[][];
weaponModsInclusive?: string[][];
weaponModsExclusive?: string[][];
enemyEquipmentInclusive?: string[][];
enemyEquipmentExclusive?: string[][];
weaponCaliber?: string[]
savageRole: string[]
savageRole?: string[]
status?: string[];
bodyPart?: string[];
daytime?: IDaytimeCounter;
@ -138,26 +142,28 @@ export interface IDaytimeCounter
export interface VisibilityCondition
{
id: string;
value: number;
dynamicLocale: boolean;
target: string
value?: number;
dynamicLocale?: boolean;
oneSessionOnly: boolean;
conditionType: string;
}
export interface IRewards
export interface IQuestRewards
{
AvailableForStart: Reward[];
AvailableForFinish: Reward[];
Started: Reward[];
Success: Reward[];
Fail: Reward[];
FailRestartable: Reward[];
Expired: Reward[];
AvailableForStart?: IQuestReward[];
AvailableForFinish?: IQuestReward[];
Started?: IQuestReward[];
Success?: IQuestReward[];
Fail?: IQuestReward[];
FailRestartable?: IQuestReward[];
Expired?: IQuestReward[];
}
export interface Reward extends Item
export interface IQuestReward
{
value?: string | number;
id: string;
id?: string;
type: QuestRewardType;
index: number;
target?: string;

View File

@ -1,27 +1,25 @@
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { IQuest, IQuestConditionTypes, IQuestRewards } from "./IQuest";
export interface IReward
export interface IRepeatableQuest extends IQuest
{
index: number;
type: string;
value: number;
target?: string;
items?: Item[];
changeCost: IChangeCost[]
changeStandingCost: number;
sptRepatableGroupName: string
}
export interface IRepeatableQuestDatabase
{
templates: ITemplates;
templates: IRepeatableTemplates;
rewards: IRewardOptions;
data: IOptions;
samples: ISampleQuests[];
}
export interface ITemplates
export interface IRepeatableTemplates
{
Elimination: IRepeatableQuest;
Completion: IRepeatableQuest;
Exploration: IRepeatableQuest;
Elimination: IQuest;
Completion: IQuest;
Exploration: IQuest;
}
export interface IPmcDataRepeatableQuest
@ -31,11 +29,9 @@ export interface IPmcDataRepeatableQuest
activeQuests: IRepeatableQuest[];
inactiveQuests: IRepeatableQuest[];
endTime: number;
changeRequirement: TChangeRequirementRecord; // what it costs to reset <QuestId, ChangeRequirement> redundant to change requirements within the IRepeatableQuest
changeRequirement: Record<string, IChangeRequirement>; // What it costs to reset <QuestId, ChangeRequirement> redundant to change requirements within IRepeatableQuest
}
export type TChangeRequirementRecord = Record<string, IChangeRequirement>;
export interface IChangeRequirement
{
changeCost: IChangeCost[];
@ -48,264 +44,6 @@ export interface IChangeCost
count: number; // amount of item needed to reset
}
export interface IRepeatableQuest
{
_id: string;
traderId: string;
location: string;
image: string;
type: string;
isKey: boolean;
restartable: boolean;
instantComplete: boolean;
secretQuest: boolean;
canShowNotificationsInGame: boolean;
rewards: IRewards;
conditions: IConditions;
side: string;
questStatus: any;
name: string;
note: string;
description: string;
successMessageText: string;
failMessageText: string;
startedMessageText: string;
changeQuestMessageText: string;
acceptPlayerMessage: string;
declinePlayerMessage: string;
completePlayerMessage: string;
templateId: string;
changeCost: IChangeCost[];
changeStandingCost: number;
sptRepatableGroupName?: string;
}
export interface IRewards
{
Started: IReward[];
Success: IReward[];
Fail: IReward[];
}
export interface IConditions
{
AvailableForStart: any[];
AvailableForFinish: IAvailableFor[];
Fail: any[];
}
export interface IAvailableFor
{
_props: IAvailableForProps;
_parent: string;
dynamicLocale: boolean;
}
export interface IAvailableForProps
{
id: string;
parentId: string;
dynamicLocale: boolean;
index: number;
visibilityConditions: IVisibilityCondition[];
value: number;
}
export interface IVisibilityCondition
{
id: string;
oneSessionOnly: boolean;
value: number;
index: number;
dynamicLocale: boolean;
}
export interface IAvailableForPropsCounter extends IAvailableForProps
{
type: string;
oneSessionOnly: boolean;
doNotResetIfCounterCompleted: boolean;
counter?: ICounter;
}
export interface ICounter
{
id: string;
conditions: ICondition[];
}
export interface ICondition
{
_props: IConditionProps;
_parent: string;
}
export interface IConditionProps
{
id: string;
dynamicLocale: boolean;
}
// Elimination
export interface IElimination extends IRepeatableQuest
{
conditions: IEliminationConditions;
}
export interface IEliminationConditions extends IConditions
{
AvailableForFinish: IEliminationAvailableFor[];
}
export interface IEliminationAvailableFor extends IAvailableFor
{
_props: IEliminationAvailableForProps;
}
export interface IEliminationAvailableForProps extends IAvailableForPropsCounter
{
counter: IEliminationCounter;
}
export interface IEliminationCounter extends ICounter
{
conditions: IEliminationCondition[];
}
export interface IEliminationCondition extends ICondition
{
_props: ILocationConditionProps | IKillConditionProps;
}
// Exploration
export interface IExploration extends IRepeatableQuest
{
conditions: IExplorationConditions;
}
export interface IExplorationConditions extends IConditions
{
AvailableForFinish: IExplorationAvailableFor[];
}
export interface IExplorationAvailableFor extends IAvailableFor
{
_props: IExplorationAvailableForProps;
}
export interface IExplorationAvailableForProps extends IAvailableForPropsCounter
{
counter: IExplorationCounter;
}
export interface IExplorationCounter extends ICounter
{
conditions: IExplorationCondition[];
}
export interface IExplorationCondition extends ICondition
{
_props: ILocationConditionProps | IExitStatusConditionProps | IExitNameConditionProps;
}
// Pickup
export interface IPickup extends IRepeatableQuest
{
conditions: IPickupConditions;
}
export interface IPickupConditions extends IConditions
{
AvailableForFinish: IPickupAvailableFor[];
}
export interface IPickupAvailableFor extends IAvailableFor
{
_props: IPickupAvailableForProps;
}
export interface IPickupAvailableForProps extends IAvailableForPropsCounter
{
target: string[];
counter?: IPickupCounter;
}
export interface IPickupCounter extends ICounter
{
conditions: IPickupCondition[];
}
export interface IPickupCondition extends ICondition
{
_props: IEquipmentConditionProps | ILocationConditionProps | IExitStatusConditionProps;
}
// Completion
export interface ICompletion extends IRepeatableQuest
{
conditions: ICompletionConditions;
}
export interface ICompletionConditions extends IConditions
{
AvailableForFinish: ICompletionAvailableFor[];
}
export interface ICompletionAvailableFor extends IAvailableFor
{
_props: ICompletionAvailableForProps;
}
export interface ICompletionAvailableForProps extends IAvailableForProps
{
target: string[];
minDurability: number;
maxDurability: number;
dogtagLevel: number;
onlyFoundInRaid: boolean;
}
// Quest Conditions
export interface ILocationConditionProps extends IConditionProps
{
target: string[];
weapon?: string[];
weaponCategories?: string[];
}
export interface IEquipmentConditionProps extends IConditionProps
{
equipmentInclusive: [string[]];
IncludeNotEquippedItems: boolean;
}
export interface IKillConditionProps extends IConditionProps
{
target: string;
value: number;
savageRole?: string[];
bodyPart?: string[];
distance?: IDistanceCheck;
weapon?: string[];
weaponCategories?: string[];
}
export interface IDistanceCheck
{
compareMethod: string;
value: number;
}
export interface IExitStatusConditionProps extends IConditionProps
{
status: string[];
}
export interface IExitNameConditionProps extends IConditionProps
{
exitName: string;
}
// Config Options
export interface IRewardOptions
@ -348,8 +86,8 @@ export interface ISampleQuests
instantComplete: boolean;
secretQuest: boolean;
canShowNotificationsInGame: boolean;
rewards: IRewards;
conditions: IConditions;
rewards: IQuestRewards;
conditions: IQuestConditionTypes;
name: string;
note: string;
description: string;