forked from OpenWF/SpaceNinjaServer
907 lines
40 KiB
TypeScript
907 lines
40 KiB
TypeScript
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"
|
||
};
|