feat: bounty standing reward (#1556)
Some checks failed
Build / build (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled

Re #388
I think this only missing `Field Bounties` and `Arcana Isolation Vault`

Reviewed-on: #1556
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
This commit is contained in:
AMelonInsideLemon 2025-04-12 06:13:44 -07:00 committed by Sainan
parent 355de3fa04
commit 946f3129b8
6 changed files with 149 additions and 70 deletions

View File

@ -1,10 +1,9 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; import { addMiscItems, addStanding, getInventory } from "@/src/services/inventoryService";
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportResources, ExportSyndicates } from "warframe-public-export-plus"; import { ExportResources } from "warframe-public-export-plus";
export const fishmongerController: RequestHandler = async (req, res) => { export const fishmongerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -31,32 +30,15 @@ export const fishmongerController: RequestHandler = async (req, res) => {
miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 }); miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
} }
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
if (gainedStanding && syndicateTag) { let affiliationMod;
let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag); if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding);
if (!syndicate) {
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0 }) - 1];
}
const syndicateMeta = ExportSyndicates[syndicateTag];
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) {
gainedStanding = max - syndicate.Standing;
}
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
syndicate.Standing += gainedStanding;
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
MiscItems: miscItemChanges MiscItems: miscItemChanges
}, },
SyndicateTag: syndicateTag, SyndicateTag: syndicateTag,
StandingChange: gainedStanding StandingChange: affiliationMod?.Standing || 0
}); });
}; };

View File

@ -55,8 +55,10 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport); const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
// skip mission rewards if not GS_SUCCESS and not a bounty (by presence of jobId, as there's a reward every stage but only the last stage has GS_SUCCESS) if (
if (missionReport.MissionStatus !== "GS_SUCCESS" && !missionReport.RewardInfo?.jobId) { missionReport.MissionStatus !== "GS_SUCCESS" &&
!(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
) {
await inventory.save(); await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true); const inventoryResponse = await getInventoryResponse(inventory, true);
res.json({ res.json({
@ -66,7 +68,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
return; return;
} }
const { MissionRewards, inventoryChanges, credits } = await addMissionRewards(inventory, missionReport); const { MissionRewards, inventoryChanges, credits, AffiliationMods, SyndicateXPItemReward } =
await addMissionRewards(inventory, missionReport);
await inventory.save(); await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true); const inventoryResponse = await getInventoryResponse(inventory, true);
@ -78,7 +81,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
MissionRewards, MissionRewards,
...credits, ...credits,
...inventoryUpdates, ...inventoryUpdates,
FusionPoints: inventoryChanges?.FusionPoints FusionPoints: inventoryChanges?.FusionPoints,
SyndicateXPItemReward,
AffiliationMods
}); });
}; };

View File

@ -1,16 +1,9 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import { addMiscItems, addStanding, freeUpSlot, getInventory } from "@/src/services/inventoryService";
addMiscItems,
freeUpSlot,
getInventory,
getStandingLimit,
updateStandingLimit
} from "@/src/services/inventoryService";
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus"; import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
@ -61,38 +54,13 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res)
inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 }; inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 };
} }
let syndicate = inventory.Affiliations.find(x => x.Tag == request.Operation.AffiliationTag); const affiliationMod = addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, true);
if (!syndicate) {
syndicate =
inventory.Affiliations[
inventory.Affiliations.push({ Tag: request.Operation.AffiliationTag, Standing: 0 }) - 1
];
}
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) {
gainedStanding = max - syndicate.Standing;
}
if (syndicateMeta.medallionsCappedByDailyLimit) {
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
syndicate.Standing += gainedStanding;
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,
AffiliationMods: [ AffiliationMods: [affiliationMod]
{
Tag: request.Operation.AffiliationTag,
Standing: gainedStanding
}
]
}); });
}; };

View File

@ -65,6 +65,7 @@ import { handleBundleAcqusition } from "./purchaseService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { getRandomElement, getRandomInt, SRng } from "./rngService"; import { getRandomElement, getRandomInt, SRng } from "./rngService";
import { createMessage } from "./inboxService"; import { createMessage } from "./inboxService";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
export const createInventory = async ( export const createInventory = async (
accountOwnerId: Types.ObjectId, accountOwnerId: Types.ObjectId,
@ -930,23 +931,50 @@ const standingLimitBinToInventoryKey: Record<
export const allDailyAffiliationKeys: (keyof IDailyAffiliations)[] = Object.values(standingLimitBinToInventoryKey); export const allDailyAffiliationKeys: (keyof IDailyAffiliations)[] = Object.values(standingLimitBinToInventoryKey);
export const getStandingLimit = (inventory: IDailyAffiliations, bin: TStandingLimitBin): number => { const getStandingLimit = (inventory: IDailyAffiliations, bin: TStandingLimitBin): number => {
if (bin == "STANDING_LIMIT_BIN_NONE" || config.noDailyStandingLimits) { if (bin == "STANDING_LIMIT_BIN_NONE" || config.noDailyStandingLimits) {
return Number.MAX_SAFE_INTEGER; return Number.MAX_SAFE_INTEGER;
} }
return inventory[standingLimitBinToInventoryKey[bin]]; return inventory[standingLimitBinToInventoryKey[bin]];
}; };
export const updateStandingLimit = ( const updateStandingLimit = (inventory: IDailyAffiliations, bin: TStandingLimitBin, subtrahend: number): void => {
inventory: IDailyAffiliations,
bin: TStandingLimitBin,
subtrahend: number
): void => {
if (bin != "STANDING_LIMIT_BIN_NONE" && !config.noDailyStandingLimits) { if (bin != "STANDING_LIMIT_BIN_NONE" && !config.noDailyStandingLimits) {
inventory[standingLimitBinToInventoryKey[bin]] -= subtrahend; inventory[standingLimitBinToInventoryKey[bin]] -= subtrahend;
} }
}; };
export const addStanding = (
inventory: TInventoryDatabaseDocument,
syndicateTag: string,
gainedStanding: number,
isMedallion: boolean = false
): IAffiliationMods => {
let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
const syndicateMeta = ExportSyndicates[syndicateTag];
if (!syndicate) {
syndicate =
inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0, Title: 0 }) - 1];
}
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing;
if (!isMedallion || (isMedallion && syndicateMeta.medallionsCappedByDailyLimit)) {
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
syndicate.Standing += gainedStanding;
return {
Tag: syndicateTag,
Standing: gainedStanding
};
};
// TODO: AffiliationMods support (Nightwave). // TODO: AffiliationMods support (Nightwave).
export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise<IUpdateNodeIntrosResponse> => { export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise<IUpdateNodeIntrosResponse> => {
const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems"); const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems");

View File

@ -28,13 +28,14 @@ import {
addMods, addMods,
addRecipes, addRecipes,
addShipDecorations, addShipDecorations,
addStanding,
combineInventoryChanges, combineInventoryChanges,
updateCurrency, updateCurrency,
updateSyndicate updateSyndicate
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { updateQuestKey } from "@/src/services/questService"; import { updateQuestKey } from "@/src/services/questService";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes";
import { getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService"; import { getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { getEntriesUnsafe } from "@/src/utils/ts-utils";
@ -529,6 +530,8 @@ interface AddMissionRewardsReturnType {
MissionRewards: IMissionReward[]; MissionRewards: IMissionReward[];
inventoryChanges?: IInventoryChanges; inventoryChanges?: IInventoryChanges;
credits?: IMissionCredits; credits?: IMissionCredits;
AffiliationMods?: IAffiliationMods[];
SyndicateXPItemReward?: number;
} }
//TODO: return type of partial missioninventoryupdate response //TODO: return type of partial missioninventoryupdate response
@ -555,6 +558,8 @@ export const addMissionRewards = async (
const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier); const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier);
logger.debug("random mission drops:", MissionRewards); logger.debug("random mission drops:", MissionRewards);
const inventoryChanges: IInventoryChanges = {}; const inventoryChanges: IInventoryChanges = {};
const AffiliationMods: IAffiliationMods[] = [];
let SyndicateXPItemReward;
let missionCompletionCredits = 0; let missionCompletionCredits = 0;
//inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display //inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display
@ -718,7 +723,97 @@ export const addMissionRewards = async (
inventoryChanges.Nemesis.InfNodes = inventory.Nemesis.InfNodes; inventoryChanges.Nemesis.InfNodes = inventory.Nemesis.InfNodes;
} }
} }
return { inventoryChanges, MissionRewards, credits };
if (rewardInfo.JobStage != undefined && rewardInfo.jobId) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [jobType, tierStr, hubNode, syndicateId, locationTag] = rewardInfo.jobId.split("_");
const tier = Number(tierStr);
const worldState = getWorldState();
let syndicateEntry = worldState.SyndicateMissions.find(m => m._id.$oid === syndicateId);
if (!syndicateEntry) syndicateEntry = worldState.SyndicateMissions.find(m => m.Tag === syndicateId); // Sometimes syndicateId can be tag
if (syndicateEntry && syndicateEntry.Jobs) {
let currentJob = syndicateEntry.Jobs[tier];
if (syndicateEntry.Tag === "EntratiSyndicate") {
const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag);
if (vault) currentJob = vault;
let medallionAmount = currentJob.xpAmounts[rewardInfo.JobStage];
if (
["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some(
ending => jobType.endsWith(ending)
)
) {
const endlessJob = syndicateEntry.Jobs.find(j => j.endless);
if (endlessJob) {
const index = rewardInfo.JobStage % endlessJob.xpAmounts.length;
const excess = Math.floor(rewardInfo.JobStage / endlessJob.xpAmounts.length);
medallionAmount = Math.floor(endlessJob.xpAmounts[index] * (1 + 0.15000001 * excess));
}
}
await addItem(inventory, "/Lotus/Types/Items/Deimos/EntratiFragmentUncommonB", medallionAmount);
MissionRewards.push({
StoreItem: "/Lotus/StoreItems/Types/Items/Deimos/EntratiFragmentUncommonB",
ItemCount: medallionAmount
});
SyndicateXPItemReward = medallionAmount;
} else {
if (tier >= 0) {
AffiliationMods.push(
addStanding(inventory, syndicateEntry.Tag, currentJob.xpAmounts[rewardInfo.JobStage])
);
} else {
if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && rewardInfo.JobStage === 2) {
AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 1000));
}
if (jobType.endsWith("Hunts/AllTeralystsHunt") && rewardInfo.JobStage === 2) {
AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 5000));
}
if (
[
"Hunts/TeralystHunt",
"Heists/HeistProfitTakerBountyTwo",
"Heists/HeistProfitTakerBountyThree",
"Heists/HeistProfitTakerBountyFour",
"Heists/HeistExploiterBountyOne"
].some(ending => jobType.endsWith(ending))
) {
AffiliationMods.push(addStanding(inventory, syndicateEntry.Tag, 1000));
}
}
}
}
}
if (rewardInfo.challengeMissionId) {
const [syndicateTag, tierStr] = rewardInfo.challengeMissionId.split("_"); // TODO: third part in HexSyndicate jobs - Chemistry points
const tier = Number(tierStr);
const isSteelPath = missions?.Tier;
if (syndicateTag === "ZarimanSyndicate") {
let medallionAmount = tier + 1;
if (isSteelPath) medallionAmount = Math.round(medallionAmount * 1.5);
await addItem(inventory, "/Lotus/Types/Gameplay/Zariman/Resources/ZarimanDogTagBounty", medallionAmount);
MissionRewards.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Zariman/Resources/ZarimanDogTagBounty",
ItemCount: medallionAmount
});
SyndicateXPItemReward = medallionAmount;
} else {
let standingAmount = (tier + 1) * 1000;
if (tier > 5) standingAmount = 7500; // InfestedLichBounty
if (isSteelPath) standingAmount *= 1.5;
AffiliationMods.push(addStanding(inventory, syndicateTag, standingAmount));
}
if (isSteelPath) {
await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1);
MissionRewards.push({
StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence",
ItemCount: 1
});
}
}
return { inventoryChanges, MissionRewards, credits, AffiliationMods, SyndicateXPItemReward };
}; };
interface IMissionCredits { interface IMissionCredits {

View File

@ -150,6 +150,7 @@ export interface IRewardInfo {
JobStage?: number; JobStage?: number;
Q?: boolean; // likely indicates that the bonus objective for this stage was completed Q?: boolean; // likely indicates that the bonus objective for this stage was completed
CheckpointCounter?: number; // starts at 1, is incremented with each job stage upload, and does not reset when starting a new job CheckpointCounter?: number; // starts at 1, is incremented with each job stage upload, and does not reset when starting a new job
challengeMissionId?: string;
} }
export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED"; export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED";