SpaceNinjaServer/missionInventoryUpdateService.ts
2025-03-18 07:14:16 -07:00

907 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
ExportEnemies,
ExportFusionBundles,
ExportRegions,
ExportRewards,
IMissionReward as IMissionRewardExternal,
IReward
} from "warframe-public-export-plus";
import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
import { logger } from "@/src/utils/logger";
import { IRngResult, getRandomElement, getRandomReward } from "@/src/services/rngService";
import { equipmentKeys, IInventoryDatabase, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import {
addChallenges,
addConsumables,
addCrewShipAmmo,
addCrewShipRawSalvage,
addEmailItem,
addFocusXpIncreases,
addFusionTreasures,
addGearExpByCategory,
addItem,
addMiscItems,
addMissionComplete,
addMods,
addRecipes,
combineInventoryChanges,
updateSyndicate
} from "@/src/services/inventoryService";
import { updateQuestKey } from "@/src/services/questService";
import { HydratedDocument } from "mongoose";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getLevelKeyRewards, getNode, toStoreItem } from "@/src/services/itemDataService";
import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { handleStoreItemAcquisition } from "./purchaseService";
import { IMissionReward } from "../types/missionTypes";
import { crackRelic } from "@/src/helpers/relicHelper";
import { createMessage } from "./inboxService";
import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.json";
import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json";
import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json";
const getRotations = (rotationCount: number): number[] => {
if (rotationCount === 0) return [0];
const rotationPattern = [0, 0, 1, 2]; // A, A, B, C
const rotatedValues = [];
for (let i = 0; i < rotationCount; i++) {
rotatedValues.push(rotationPattern[i % rotationPattern.length]);
}
return rotatedValues;
};
const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => {
return getRandomReward(pool as IRngResult[]);
};
//type TMissionInventoryUpdateKeys = keyof IMissionInventoryUpdateRequest;
//const ignoredInventoryUpdateKeys = ["FpsAvg", "FpsMax", "FpsMin", "FpsSamples"] satisfies TMissionInventoryUpdateKeys[]; // for keys with no meaning for this server
//type TignoredInventoryUpdateKeys = (typeof ignoredInventoryUpdateKeys)[number];
//const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys
export const addMissionInventoryUpdates = async (
inventory: HydratedDocument<IInventoryDatabase, InventoryDocumentProps>,
inventoryUpdates: IMissionInventoryUpdateRequest
): Promise<IInventoryChanges> => {
const inventoryChanges: IInventoryChanges = {};
if (inventoryUpdates.RewardInfo && inventoryUpdates.RewardInfo.periodicMissionTag) {
const tag = inventoryUpdates.RewardInfo.periodicMissionTag;
const existingCompletion = inventory.PeriodicMissionCompletions.find(completion => completion.tag === tag);
if (existingCompletion) {
existingCompletion.date = new Date();
} else {
inventory.PeriodicMissionCompletions.push({
tag: tag,
date: new Date()
});
}
}
if (inventoryUpdates.RewardInfo && inventoryUpdates.RewardInfo.NemesisAbandonedRewards) {
inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards;
}
for (const [key, value] of getEntriesUnsafe(inventoryUpdates)) {
if (value === undefined) {
logger.error(`Inventory update key ${key} has no value `);
continue;
}
switch (key) {
case "RegularCredits":
inventory.RegularCredits += value;
break;
case "QuestKeys":
await updateQuestKey(inventory, value);
break;
case "AffiliationChanges":
updateSyndicate(inventory, value);
break;
// Incarnon Challenges
case "EvolutionProgress": {
for (const evoProgress of value) {
const entry = inventory.EvolutionProgress
? inventory.EvolutionProgress.find(entry => entry.ItemType == evoProgress.ItemType)
: undefined;
if (entry) {
entry.Progress = evoProgress.Progress;
entry.Rank = evoProgress.Rank;
} else {
inventory.EvolutionProgress ??= [];
inventory.EvolutionProgress.push(evoProgress);
}
}
break;
}
case "Missions":
addMissionComplete(inventory, value);
break;
case "LastRegionPlayed":
inventory.LastRegionPlayed = value;
break;
case "RawUpgrades":
addMods(inventory, value);
break;
case "MiscItems":
case "BonusMiscItems":
addMiscItems(inventory, value);
break;
case "Consumables":
addConsumables(inventory, value);
break;
case "Recipes":
addRecipes(inventory, value);
break;
case "ChallengeProgress":
addChallenges(inventory, value);
break;
case "FusionTreasures":
addFusionTreasures(inventory, value);
break;
case "CrewShipRawSalvage":
addCrewShipRawSalvage(inventory, value);
break;
case "CrewShipAmmo":
addCrewShipAmmo(inventory, value);
break;
case "FusionBundles": {
let fusionPoints = 0;
for (const fusionBundle of value) {
const fusionPointsTotal =
ExportFusionBundles[fusionBundle.ItemType].fusionPoints * fusionBundle.ItemCount;
inventory.FusionPoints += fusionPointsTotal;
fusionPoints += fusionPointsTotal;
}
inventoryChanges.FusionPoints = fusionPoints;
break;
}
case "EmailItems": {
for (const tc of value) {
await addEmailItem(inventory, tc.ItemType);
}
break;
}
case "FocusXpIncreases": {
addFocusXpIncreases(inventory, value);
break;
}
case "PlayerSkillGains": {
inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE;
inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER;
break;
}
case "CustomMarkers": {
value.forEach(markers => {
const map = inventory.CustomMarkers
? inventory.CustomMarkers.find(entry => entry.tag == markers.tag)
: undefined;
if (map) {
map.markerInfos = markers.markerInfos;
} else {
inventory.CustomMarkers ??= [];
inventory.CustomMarkers.push(markers);
}
});
break;
}
case "LoreFragmentScans":
value.forEach(clientFragment => {
const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType);
if (fragment) {
fragment.Progress += clientFragment.Progress;
} else {
inventory.LoreFragmentScans.push(clientFragment);
}
});
break;
case "LibraryScans":
value.forEach(scan => {
let synthesisIgnored = true;
if (
inventory.LibraryPersonalTarget &&
libraryPersonalTargetToAvatar[inventory.LibraryPersonalTarget] == scan.EnemyType
) {
let progress = inventory.LibraryPersonalProgress.find(
x => x.TargetType == inventory.LibraryPersonalTarget
);
if (!progress) {
progress =
inventory.LibraryPersonalProgress[
inventory.LibraryPersonalProgress.push({
TargetType: inventory.LibraryPersonalTarget,
Scans: 0,
Completed: false
}) - 1
];
}
progress.Scans += scan.Count;
if (
progress.Scans >=
(inventory.LibraryPersonalTarget ==
"/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget"
? 3
: 10)
) {
progress.Completed = true;
}
logger.debug(`synthesis of ${scan.EnemyType} added to personal target progress`);
synthesisIgnored = false;
}
if (
inventory.LibraryActiveDailyTaskInfo &&
inventory.LibraryActiveDailyTaskInfo.EnemyTypes.find(x => x == scan.EnemyType)
) {
inventory.LibraryActiveDailyTaskInfo.Scans ??= 0;
inventory.LibraryActiveDailyTaskInfo.Scans += scan.Count;
logger.debug(`synthesis of ${scan.EnemyType} added to daily task progress`);
synthesisIgnored = false;
}
if (synthesisIgnored) {
logger.warn(`ignoring synthesis of ${scan.EnemyType} due to not knowing why you did that`);
}
});
break;
case "CollectibleScans":
for (const scan of value) {
const entry = inventory.CollectibleSeries?.find(x => x.CollectibleType == scan.CollectibleType);
if (entry) {
entry.Count = scan.Count;
entry.Tracking = scan.Tracking;
if (entry.CollectibleType == "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne") {
const progress = entry.Count / entry.ReqScans;
for (const gate of entry.IncentiveStates) {
gate.complete = progress >= gate.threshold;
if (gate.complete && !gate.sent) {
gate.sent = true;
if (gate.threshold == 0.5) {
await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage50]);
} else {
await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage75]);
}
}
}
if (progress >= 1.0) {
await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage100]);
}
}
} else {
logger.warn(`${scan.CollectibleType} was not found in inventory, ignoring scans`);
}
}
break;
case "Upgrades":
value.forEach(clientUpgrade => {
const upgrade = inventory.Upgrades.id(clientUpgrade.ItemId.$oid)!;
upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress
});
break;
case "SyndicateId": {
inventory.CompletedSyndicates.push(value);
break;
}
case "SortieId": {
inventory.CompletedSorties.push(value);
break;
}
case "SeasonChallengeCompletions": {
const processedCompletions = value.map(({ challenge, id }) => ({
challenge: challenge.substring(challenge.lastIndexOf("/") + 1),
id
}));
inventory.SeasonChallengeHistory.push(...processedCompletions);
break;
}
case "DeathMarks": {
for (const deathMark of value) {
if (!inventory.DeathMarks.find(x => x == deathMark)) {
// It's a new death mark; we have to say the line.
await createMessage(inventory.accountOwnerId.toString(), [
{
sub: "/Lotus/Language/G1Quests/DeathMarkTitle",
sndr: "/Lotus/Language/G1Quests/DeathMarkSender",
msg: "/Lotus/Language/G1Quests/DeathMarkMessage",
icon: "/Lotus/Interface/Icons/Npcs/Stalker_d.png",
highPriority: true
}
]);
// TODO: This type of inbox message seems to automatically delete itself. Figure out under which conditions.
}
}
inventory.DeathMarks = value;
break;
}
default:
// Equipment XP updates
if (equipmentKeys.includes(key as TEquipmentKey)) {
const multipliedValue = (value as IEquipmentClient[]).map(equipment => {
// 生成一个 0 到 1 的随机数
const randomChance = Math.random();
// 基础倍率为 5 倍
let multiplier = 5;
// 10% 的概率触发 10 倍经验
if (randomChance < 0.1) {
multiplier = 10; // ✅ 10% 概率 10 倍经验
}
// 计算最终经验值
const finalXP = (equipment.XP ?? 0) * multiplier;
// 日志输出(中文)
logger.debug(`[经验倍率] 随机数: ${randomChance.toFixed(2)}, 倍率: ${multiplier}, 最终经验值: ${finalXP}`);
return {
...equipment,
XP: finalXP // ✅ 处理 undefined
};
});
addGearExpByCategory(inventory, multipliedValue, key as TEquipmentKey);
}
break;
// if (
// (ignoredInventoryUpdateKeys as readonly string[]).includes(key) ||
// knownUnhandledKeys.includes(key)
// ) {
// continue;
// }
// logger.error(`Unhandled inventory update key: ${key}`);
}
}
return inventoryChanges;
};
interface AddMissionRewardsReturnType {
MissionRewards: IMissionReward[];
inventoryChanges?: IInventoryChanges;
credits?: IMissionCredits;
}
//TODO: return type of partial missioninventoryupdate response
export const addMissionRewards = async (
inventory: TInventoryDatabaseDocument,
{
RewardInfo: rewardInfo,
LevelKeyName: levelKeyName,
Missions: missions,
RegularCredits: creditDrops,
VoidTearParticipantsCurrWave: voidTearWave,
StrippedItems: strippedItems
}: IMissionInventoryUpdateRequest
): Promise<AddMissionRewardsReturnType> => {
if (!rewardInfo) {
//TODO: if there is a case where you can have credits collected during a mission but no rewardInfo, add credits needs to be handled earlier
logger.debug(`Mission ${missions!.Tag} did not have Reward Info `);
return { MissionRewards: [] };
}
//TODO: check double reward merging
const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo);
logger.debug("random mission drops:", MissionRewards);
const inventoryChanges: IInventoryChanges = {};
let missionCompletionCredits = 0;
//inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display
if (levelKeyName) {
const fixedLevelRewards = getLevelKeyRewards(levelKeyName);
//logger.debug(`fixedLevelRewards ${fixedLevelRewards}`);
if (fixedLevelRewards.levelKeyRewards) {
addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards);
}
if (fixedLevelRewards.levelKeyRewards2) {
for (const reward of fixedLevelRewards.levelKeyRewards2) {
//quest stage completion credit rewards
if (reward.rewardType == "RT_CREDITS") {
inventory.RegularCredits += reward.amount;
missionCompletionCredits += reward.amount;
continue;
}
MissionRewards.push({
StoreItem: reward.itemType,
ItemCount: reward.rewardType === "RT_RESOURCE" ? reward.amount : 1
});
}
}
}
if (
missions &&
missions.Tag != "" // https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1013
) {
const node = getNode(missions.Tag);
//node based credit rewards for mission completion
if (node.missionIndex !== 28) {
const levelCreditReward = getLevelCreditRewards(missions.Tag);
missionCompletionCredits += levelCreditReward;
inventory.RegularCredits += levelCreditReward;
logger.debug(`levelCreditReward ${levelCreditReward}`);
}
if (node.missionReward) {
missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards);
}
}
if (rewardInfo.useVaultManifest) {
MissionRewards.push({
StoreItem: getRandomElement(corruptedMods),
ItemCount: 1
});
}
for (const reward of MissionRewards) {
const inventoryChange = await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount);
//TODO: combineInventoryChanges improve type safety, merging 2 of the same item?
//TODO: check for the case when two of the same item are added, combineInventoryChanges should merge them, but the client also merges them
//TODO: some conditional types to rule out binchanges?
combineInventoryChanges(inventoryChanges, inventoryChange.InventoryChanges);
}
const credits = addCredits(inventory, {
missionCompletionCredits,
missionDropCredits: creditDrops ?? 0,
rngRewardCredits: inventoryChanges.RegularCredits ?? 0
});
if (
voidTearWave &&
voidTearWave.Participants[0].QualifiesForReward &&
!voidTearWave.Participants[0].HaveRewardResponse
) {
const reward = await crackRelic(inventory, voidTearWave.Participants[0], inventoryChanges);
MissionRewards.push({ StoreItem: reward.type, ItemCount: reward.itemCount });
}
if (strippedItems) {
for (const si of strippedItems) {
const droptable = ExportEnemies.droptables[si.DropTable];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!droptable) {
logger.error(`unknown droptable ${si.DropTable}`);
} else {
for (let i = 0; i != (si.DROP_MOD || []).length; ++i) {
for (const pool of droptable) {
const reward = getRandomReward(pool.items)!;
logger.debug(`stripped droptable rolled`, reward);
await addItem(inventory, reward.type);
MissionRewards.push({
StoreItem: toStoreItem(reward.type),
ItemCount: 1,
FromEnemyCache: true // to show "identified"
});
}
}
}
}
}
return { inventoryChanges, MissionRewards, credits };
};
interface IMissionCredits {
MissionCredits: number[];
CreditBonus: number[];
TotalCredits: number[];
DailyMissionBonus?: boolean;
}
//creditBonus is not entirely accurate.
//TODO: consider ActiveBoosters
export const addCredits = (
inventory: HydratedDocument<IInventoryDatabase>,
{
missionDropCredits,
missionCompletionCredits,
rngRewardCredits
}: { missionDropCredits: number; missionCompletionCredits: number; rngRewardCredits: number }
): IMissionCredits => {
const hasDailyCreditBonus = true;
const totalCredits = missionDropCredits + missionCompletionCredits + rngRewardCredits;
const finalCredits: IMissionCredits = {
MissionCredits: [missionDropCredits, missionDropCredits],
CreditBonus: [missionCompletionCredits, missionCompletionCredits],
TotalCredits: [totalCredits, totalCredits]
};
if (hasDailyCreditBonus) {
inventory.RegularCredits += missionCompletionCredits;
finalCredits.CreditBonus[1] *= 2;
finalCredits.MissionCredits[1] *= 2;
finalCredits.TotalCredits[1] *= 2;
}
if (!hasDailyCreditBonus) {
return finalCredits;
}
return { ...finalCredits, DailyMissionBonus: true };
};
export const addFixedLevelRewards = (
rewards: IMissionRewardExternal,
inventory: TInventoryDatabaseDocument,
MissionRewards: IMissionReward[]
): number => {
let missionBonusCredits = 0;
if (rewards.credits) {
missionBonusCredits += rewards.credits;
inventory.RegularCredits += rewards.credits;
}
if (rewards.items) {
for (const item of rewards.items) {
MissionRewards.push({
StoreItem: item,
ItemCount: 1
});
}
}
if (rewards.countedItems) {
for (const item of rewards.countedItems) {
MissionRewards.push({
StoreItem: `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`,
ItemCount: item.ItemCount
});
}
}
if (rewards.countedStoreItems) {
for (const item of rewards.countedStoreItems) {
MissionRewards.push(item);
}
}
return missionBonusCredits;
};
function getLevelCreditRewards(nodeName: string): number {
const minEnemyLevel = getNode(nodeName).minEnemyLevel;
return 1000 + (minEnemyLevel - 1) * 100;
//TODO: get dark sektor fixed credit rewards and railjack bonus
}
function getRandomMissionDrops(RewardInfo: IRewardInfo): IMissionReward[] {
const drops: IMissionReward[] = [];
// 模糊匹配 jobId 并处理奖励
if (RewardInfo.jobId) {
// 定义任务类型和对应的奖励表、声望阵营及声望值
const jobRewardsMap: Record<string, { rewardManifest: string; tag: string; standingValue: number }> = {
"AssassinateBountyCap": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATableCRewards",
tag: "CetusSyndicate",
standingValue: 430
},
"DeimosGrnSurvivorBounty": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATableBRewards",
tag: "EntratiSyndicate",
standingValue: 450
},
"DeimosAreaDefenseBounty": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTableBRewards",
tag: "EntratiSyndicate",
standingValue: 500
},
"DeimosEndlessExcavateBounty": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableARewards",
tag: "EntratiSyndicate",
standingValue: 550
},
"DeimosAssassinateBounty": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTableBRewards",
tag: "EntratiSyndicate",
standingValue: 600
},
"DeimosKeyPiecesBounty": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards",
tag: "EntratiSyndicate",
standingValue: 650
},
"DeimosExcavateBounty": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards",
tag: "EntratiSyndicate",
standingValue: 700
},
"VenusIntelJobSpy": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATableBRewards",
tag: "SolarisSyndicate",
standingValue: 450
},
"VenusCullJobResource": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTableBRewards",
tag: "SolarisSyndicate",
standingValue: 500
},
"VenusIntelJobRecovery": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTableBRewards",
tag: "SolarisSyndicate",
standingValue: 550
},
"VenusHelpingJobCaches": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTableBRewards",
tag: "SolarisSyndicate",
standingValue: 600
},
"VenusArtifactJobAmbush": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards",
tag: "SolarisSyndicate",
standingValue: 650
},
"VenusChaosJobExcavation": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards",
tag: "SolarisSyndicate",
standingValue: 700
},
"NarmerVenusCullJobExterminate": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards",
tag: "SolarisSyndicate",
standingValue: 800
},
"AttritionBountyLib": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTableBRewards",
tag: "CetusSyndicate",
standingValue: 500
},
"RescueBountyResc": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTableBRewards",
tag: "CetusSyndicate",
standingValue: 550
},
"CaptureBountyCapTwo": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTableBRewards",
tag: "CetusSyndicate",
standingValue: 600
},
"ReclamationBountyCache": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards",
tag: "CetusSyndicate",
standingValue: 650
},
"AttritionBountyCap": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards",
tag: "CetusSyndicate",
standingValue: 700
},
"ChamberB": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATableCRewards",
tag: "EntratiSyndicate",
standingValue: 500
},
"ChamberA": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTableCRewards",
tag: "EntratiSyndicate",
standingValue: 800
},
"Chamberc": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTableCRewards",
tag: "EntratiSyndicate",
standingValue: 1000
},
"HeistProfitTakerBountyOne": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTableCRewards111",
tag: "EntratiSyndicate",
standingValue: 1000
},
"AssassinateBountyAss": {
rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards",
tag: "CetusSyndicate",
standingValue: 800
}
};
// 遍历任务类型,模糊匹配 jobId
for (const [jobType, { rewardManifest, tag, standingValue }] of Object.entries(jobRewardsMap)) {
if (RewardInfo.jobId.includes(jobType)) {
logger.debug(`Job ID contains ${jobType}, using reward manifest: ${rewardManifest}`);
const rewardTable = ExportRewards[rewardManifest];
if (rewardTable) {
// 使用 JobStage 作为轮次索引
let rotation = RewardInfo.JobStage || 0; // 默认值为 0
logger.debug("Using JobStage as rotation index:", rotation);
// 如果 JobStage 超过 3则按最高档第 3 档)处理
if (rotation > 3) {
rotation = 3;
logger.debug("JobStage exceeds 3, using highest rotation (3)");
}
// 检查轮次索引是否在奖励表范围内
if (rotation >= rewardTable.length || rotation < 0) {
logger.error(`Rotation index ${rotation} is out of bounds for reward table ${rewardManifest}`);
} else {
// 获取当前轮次的奖励池
const rotationRewards = rewardTable[rotation];
logger.debug("Rotation rewards:", rotationRewards);
// 从奖励池中随机选择一个奖励
const drop = getRandomRewardByChance(rotationRewards);
if (drop) {
logger.debug("Random drop selected:", drop);
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
} else {
logger.debug("No drop selected from reward table");
}
}
} else {
logger.error(`Reward table ${rewardManifest} not found in ExportRewards`);
}
// 新增一个固定的物品奖励
const additionalReward = {
StoreItem: "/Lotus/StoreItems/Types/Items/SyndicateDogTags/UniversalSyndicateDogTag", // 新物品的路径
ItemCount: 1 // 物品数量
};
drops.push(additionalReward);
logger.debug("Added additional reward:", additionalReward);
// 直接返回,不再执行后续的区域奖励逻辑
return drops;
}
}
}
if (RewardInfo.node in ExportRegions) {
const region = ExportRegions[RewardInfo.node];
const rewardManifests: string[] =
RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB"
? ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"]
: region.rewardManifests;
let rotations: number[] = [];
if (RewardInfo.VaultsCracked) {
// For Spy missions, e.g. 3 vaults cracked = A, B, C
for (let i = 0; i != RewardInfo.VaultsCracked; ++i) {
rotations.push(i);
}
} else {
const rotationCount = RewardInfo.rewardQualifications?.length || 0;
rotations = getRotations(rotationCount);
}
rewardManifests
.map(name => ExportRewards[name])
.forEach(table => {
for (const rotation of rotations) {
const rotationRewards = table[rotation];
const drop = getRandomRewardByChance(rotationRewards);
// 原始掉落逻辑
if (drop) {
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
}
// EliteAlert奖励逻辑
if (RewardInfo.periodicMissionTag === "EliteAlert" || RewardInfo.periodicMissionTag === "EliteAlertB") {
const randomCount = Math.floor(Math.random() * 5) + 1;
drops.push({ StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/Elitium", ItemCount: randomCount });
}
// 添加 HardDaily 任务的 钢铁精华 掉落
if (RewardInfo.periodicMissionTag?.startsWith("HardDaily")) {
let randomCount = Math.floor(Math.random() * 5) + 1; // 生成 1 到 5 的随机数
// 20% 的几率翻 1 到 10 倍
if (Math.random() < 0.2) {
const multiplier = Math.floor(Math.random() * 10) + 1; // 生成 1 到 10 的随机倍数
randomCount *= multiplier;
}
drops.push({ StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence", ItemCount: randomCount });
}
// 新增10%概率独立掉落 ▼▼▼
if (Math.random() < 0.01) { // 每个rotation独立判定
drops.push({
StoreItem: "/Lotus/StoreItems/Upgrades/Skins/Volt/SWTechnoshockHelmet",
ItemCount: 1
});
}
}
});
if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) {
const deck = ExportRewards[region.cacheRewardManifest];
for (let rotation = 0; rotation != RewardInfo.EnemyCachesFound; ++rotation) {
const drop = getRandomRewardByChance(deck[rotation]);
if (drop) {
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount, FromEnemyCache: true });
}
}
}
if (RewardInfo.nightmareMode) {
const deck = ExportRewards["/Lotus/Types/Game/MissionDecks/NightmareModeRewards"];
let rotation = 0;
// 确保 region 已正确初始化
if (region) {
if (region.missionIndex === 3 && RewardInfo.rewardTier) {
// 如果 missionIndex 为 3 且 rewardTier 存在,则使用 rewardTier
rotation = RewardInfo.rewardTier;
} else if ([6, 7, 8, 10, 11].includes(region.systemIndex)) {
// 如果 systemIndex 在 [6, 7, 8, 10, 11] 中,则 rotation 为 2
rotation = 2;
} else if ([4, 9, 12, 14, 15, 16, 17, 18].includes(region.systemIndex)) {
// 如果 systemIndex 在 [4, 9, 12, 14, 15, 16, 17, 18] 中,则 rotation 为 1
rotation = 1;
}
}
// 确保 rotation 在 deck 的范围内
if (rotation >= deck.length || rotation < 0) {
logger.error(`Rotation index ${rotation} is out of bounds for NightmareModeRewards`);
rotation = 0; // 如果超出范围,则使用默认值 0
}
// 获取当前轮次的奖励池
const rotationRewards = deck[rotation];
if (rotationRewards) {
// 从奖励池中随机选择一个奖励
const drop = getRandomRewardByChance(rotationRewards);
if (drop) {
logger.debug("Nightmare mode drop selected:", drop);
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
} else {
logger.debug("No drop selected from NightmareModeRewards");
}
} else {
logger.error("No rewards found for NightmareModeRewards");
}
}
}
// 确保函数有返回值
return drops;
}
const corruptedMods = [
"/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/CorruptedHeavyDamageChargeSpeedMod", // Corrupt Charge
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritDamagePistol", // Hollow Point
"/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/CorruptedDamageSpeedMod", // Spoiled Strike
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedDamageRecoilPistol", // Magnum Force
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedMaxClipReloadSpeedPistol", // Tainted Clip
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedCritRateFireRateRifle", // Critical Delay
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedDamageRecoilRifle", // Heavy Caliber
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedMaxClipReloadSpeedRifle", // Tainted Mag
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedRecoilFireRateRifle", // Vile Precision
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedDurationRangeWarframe", // Narrow Minded
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedEfficiencyDurationWarframe", // Fleeting Expertise
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedPowerEfficiencyWarframe", // Blind Rage
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedRangePowerWarframe", // Overextended
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedAccuracyFireRateShotgun", // Tainted Shell
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedDamageAccuracyShotgun", // Vicious Spread
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedMaxClipReloadSpeedShotgun", // Burdened Magazine
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedFireRateDamagePistol", // Anemic Agility
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedFireRateDamageRifle", // Vile Acceleration
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedFireRateDamageShotgun", // Frail Momentum
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedCritChanceFireRateShotgun", // Critical Deceleration
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritChanceFireRatePistol", // Creeping Bullseye
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedPowerStrengthPowerDurationWarframe", // Transient Fortitude
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedReloadSpeedMaxClipRifle", // Depleted Reload
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/FixedShieldAndShieldGatingDuration" // Catalyzing Shields
];
const libraryPersonalTargetToAvatar: Record<string, string> = {
"/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget":
"/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar",
"/Lotus/Types/Game/Library/Targets/Research1Target":
"/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar",
"/Lotus/Types/Game/Library/Targets/Research2Target":
"/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserDiscBipedAvatar",
"/Lotus/Types/Game/Library/Targets/Research3Target":
"/Lotus/Types/Enemies/Grineer/Desert/Avatars/EvisceratorLancerAvatar",
"/Lotus/Types/Game/Library/Targets/Research4Target": "/Lotus/Types/Enemies/Orokin/OrokinHealingAncientAvatar",
"/Lotus/Types/Game/Library/Targets/Research5Target":
"/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/ShotgunSpacemanAvatar",
"/Lotus/Types/Game/Library/Targets/Research6Target": "/Lotus/Types/Enemies/Infested/AiWeek/Runners/RunnerAvatar",
"/Lotus/Types/Game/Library/Targets/Research7Target":
"/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/GrineerMeleeStaffAvatar",
"/Lotus/Types/Game/Library/Targets/Research8Target": "/Lotus/Types/Enemies/Orokin/OrokinHeavyFemaleAvatar",
"/Lotus/Types/Game/Library/Targets/Research9Target":
"/Lotus/Types/Enemies/Infested/AiWeek/Quadrupeds/QuadrupedAvatar",
"/Lotus/Types/Game/Library/Targets/Research10Target":
"/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar"
};