Compare commits

..

4 Commits

Author SHA1 Message Date
a1a208844a log request body for unhandled operations 2025-03-18 11:06:41 +01:00
2af671223e note what the RNG classes are for 2025-03-18 10:02:52 +01:00
b32de0711b details 2025-03-17 21:19:00 +01:00
846906d390 feat: start nemesis 2025-03-17 21:02:58 +01:00
23 changed files with 360 additions and 2683 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,906 +0,0 @@
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"
};

View File

@ -1,100 +0,0 @@
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
import DeimosFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json";
import DeimosHivemindCommisionsManifestFishmonger from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestFishmonger.json";
import DeimosHivemindCommisionsManifestPetVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestPetVendor.json";
import DeimosHivemindCommisionsManifestProspector from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestProspector.json";
import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestTokenVendor.json";
import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json";
import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json";
import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json";
import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosProspectorVendorManifest.json";
import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json";
import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json";
import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json";
import GuildAdvertisementVendorManifest from "@/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json";
import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json";
import HubsPerrinSequenceWeaponVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json";
import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json";
import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json";
import OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json";
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json";
import SolarisDebtTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json";
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json";
import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json";
import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json";
import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json";
interface IVendorManifest {
VendorInfo: {
_id: IOid;
TypeName: string;
ItemManifest: {
StoreItem: string;
ItemPrices?: { ItemType: string; ItemCount: number; ProductCategory: string }[];
Bin: string;
QuantityMultiplier: number;
Expiry: IMongoDate;
PurchaseQuantityLimit?: number;
RotatedWeekly?: boolean;
AllowMultipurchase: boolean;
Id: IOid;
}[];
Expiry: IMongoDate;
};
}
const vendorManifests: IVendorManifest[] = [
ArchimedeanVendorManifest,
DeimosEntratiFragmentVendorProductsManifest,
DeimosFishmongerVendorManifest,
DeimosHivemindCommisionsManifestFishmonger,
DeimosHivemindCommisionsManifestPetVendor,
DeimosHivemindCommisionsManifestProspector,
DeimosHivemindCommisionsManifestTokenVendor,
DeimosHivemindCommisionsManifestWeaponsmith,
DeimosHivemindTokenVendorManifest,
DeimosPetVendorManifest,
DeimosProspectorVendorManifest,
DuviriAcrithisVendorManifest,
EntratiLabsEntratiLabsCommisionsManifest,
EntratiLabsEntratiLabVendorManifest,
GuildAdvertisementVendorManifest,
HubsIronwakeDondaVendorManifest,
HubsPerrinSequenceWeaponVendorManifest,
HubsRailjackCrewMemberVendorManifest,
MaskSalesmanManifest,
OstronFishmongerVendorManifest,
OstronPetVendorManifest,
OstronProspectorVendorManifest,
RadioLegionIntermission12VendorManifest,
SolarisDebtTokenVendorManifest,
SolarisDebtTokenVendorRepossessionsManifest,
SolarisFishmongerVendorManifest,
SolarisProspectorVendorManifest,
TeshinHardModeVendorManifest,
ZarimanCommisionsManifestArchimedean
];
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
for (const vendorManifest of vendorManifests) {
if (vendorManifest.VendorInfo.TypeName == typeName) {
return vendorManifest;
}
}
return undefined;
};
export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => {
for (const vendorManifest of vendorManifests) {
if (vendorManifest.VendorInfo._id.$oid == oid) {
return vendorManifest;
}
}
return undefined;
};

View File

@ -12,8 +12,8 @@ import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-ex
export const dronesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
if ("GetActive" in req.query) {
const inventory = await getInventory(accountId, "Drones");
const activeDrones: IActiveDrone[] = [];
for (const drone of inventory.Drones) {
if (drone.DeployTime) {
@ -39,7 +39,6 @@ export const dronesController: RequestHandler = async (req, res) => {
ActiveDrones: activeDrones
});
} else if ("droneId" in req.query && "systemIndex" in req.query) {
const inventory = await getInventory(accountId, "Drones");
const drone = inventory.Drones.id(req.query.droneId as string)!;
const droneMeta = ExportDrones[drone.ItemType];
drone.DeployTime = config.instantResourceExtractorDrones ? new Date(0) : new Date();
@ -77,7 +76,6 @@ export const dronesController: RequestHandler = async (req, res) => {
await inventory.save();
res.json({});
} else if ("collectDroneId" in req.query) {
const inventory = await getInventory(accountId);
const drone = inventory.Drones.id(req.query.collectDroneId as string)!;
if (new Date() >= drone.DamageTime!) {

View File

@ -1,22 +1,7 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const getNewRewardSeedController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const rewardSeed = generateRewardSeed();
logger.debug(`generated new reward seed: ${rewardSeed}`);
await Inventory.updateOne(
{
accountOwnerId: accountId
},
{
RewardSeed: rewardSeed
}
);
res.json({ rewardSeed: rewardSeed });
export const getNewRewardSeedController: RequestHandler = (_req, res) => {
res.json({ rewardSeed: generateRewardSeed() });
};
export function generateRewardSeed(): number {

View File

@ -23,6 +23,7 @@ import { config } from "@/src/services/configService";
import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes";
import { TGuildDatabaseDocument } from "@/src/models/guildModel";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
export const guildTechController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -219,6 +220,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
await guild.save();
res.end();
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown guildTech action: ${data.Action}`);
}
};

View File

@ -128,7 +128,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const miscItemChanges: IMiscItem[] = [];
let totalPercentagePointsGained = 0;
const currentUnixSeconds = Math.trunc(Date.now() / 1000);
const currentUnixSeconds = Math.trunc(new Date().getTime() / 1000);
for (const contribution of request.ResourceContributions) {
const snack = ExportMisc.helminthSnacks[contribution.ItemType];
@ -260,7 +260,9 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
inventory.InfestedFoundry!.ConsumedSuits ??= [];
inventory.InfestedFoundry!.ConsumedSuits.push(consumedSuit);
inventory.InfestedFoundry!.LastConsumedSuit = suit;
inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date(Date.now() + 24 * 60 * 60 * 1000);
inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date(
new Date().getTime() + 24 * 60 * 60 * 1000
);
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00);
addRecipes(inventory, recipeChanges);
freeUpSlot(inventory, InventorySlot.SUITS);
@ -308,7 +310,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!;
const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const upgradesExpiry = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000);
suit.OffensiveUpgrade = request.OffensiveUpgradeType;
suit.DefensiveUpgrade = request.DefensiveUpgradeType;
suit.UpgradesExpiry = upgradesExpiry;
@ -355,6 +357,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
}
default:
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`);
}
};

View File

@ -0,0 +1,152 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService";
import { IMongoDate } from "@/src/types/commonTypes";
import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
import { ExportRegions } from "warframe-public-export-plus";
export const nemesisController: RequestHandler = async (req, res) => {
if ((req.query.mode as string) == "s") {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Nemesis NemesisAbandonedRewards");
const body = getJSONfromString<INemesisStartRequest>(String(req.body));
const infNodes: IInfNode[] = [];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
value.systemIndex == 2 && // earth
value.nodeType != 3 && // not hub
value.nodeType != 7 && // not junction
value.missionIndex && // must have a mission type and not assassination
value.missionIndex != 28 && // not open world
value.missionIndex != 32 && // not railjack
value.missionIndex != 41 && // not saya's visions
value.name.indexOf("Archwing") == -1
) {
//console.log(dict_en[value.name]);
infNodes.push({ Node: key, Influence: 1 });
}
}
let weapons: readonly string[];
if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") {
weapons = kuvaLichVersionSixWeapons;
} else if (
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" ||
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree"
) {
weapons = corpusVersionThreeWeapons;
} else {
throw new Error(`unknown nemesis manifest: ${body.target.manifest}`);
}
body.target.fp = BigInt(body.target.fp);
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
let weaponIdx = initialWeaponIdx;
do {
const weapon = weapons[weaponIdx];
if (!body.target.DisallowedWeapons.find(x => x == weapon)) {
break;
}
weaponIdx = (weaponIdx + 1) % weapons.length;
} while (weaponIdx != initialWeaponIdx);
inventory.Nemesis = {
fp: body.target.fp,
manifest: body.target.manifest,
KillingSuit: body.target.KillingSuit,
killingDamageType: body.target.killingDamageType,
ShoulderHelmet: body.target.ShoulderHelmet,
WeaponIdx: weaponIdx,
AgentIdx: body.target.AgentIdx,
BirthNode: body.target.BirthNode,
Faction: body.target.Faction,
Rank: 0,
k: false,
Traded: false,
d: new Date(),
InfNodes: infNodes,
GuessHistory: [],
Hints: [],
HintProgress: 0,
Weakened: body.target.Weakened,
PrevOwners: 0,
HenchmenKilled: 0,
SecondInCommand: body.target.SecondInCommand
};
inventory.NemesisAbandonedRewards = []; // unclear if we need to do this since the client also submits this with missionInventoryUpdate
await inventory.save();
res.json({
target: inventory.toJSON().Nemesis
});
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`);
}
};
export interface INemesisStartRequest {
target: {
fp: number | bigint;
manifest: string;
KillingSuit: string;
killingDamageType: number;
ShoulderHelmet: string;
DisallowedWeapons: string[];
WeaponIdx: number;
AgentIdx: number;
BirthNode: string;
Faction: string;
Rank: number;
k: boolean;
Traded: boolean;
d: IMongoDate;
InfNodes: [];
GuessHistory: [];
Hints: [];
HintProgress: number;
Weakened: boolean;
PrevOwners: number;
HenchmenKilled: number;
SecondInCommand: boolean;
};
}
const kuvaLichVersionSixWeapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
"/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
"/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
];
const corpusVersionThreeWeapons = [
"/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
"/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
];

View File

@ -5,7 +5,6 @@ import { IMongoDate } from "@/src/types/commonTypes";
import { RequestHandler } from "express";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { createMessage } from "@/src/services/inboxService";
interface ITrainingResultsRequest {
numLevelsGained: number;
@ -27,25 +26,6 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
if (trainingResults.numLevelsGained == 1) {
inventory.TrainingDate = new Date(Date.now() + unixTimesInMs.hour * 23);
inventory.PlayerLevel += 1;
await createMessage(accountId, [
{
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
msg: "/Lotus/Language/Inbox/MasteryRewardMsg",
arg: [
{
Key: "NEW_RANK",
Tag: inventory.PlayerLevel
}
],
att: [
`/Lotus/Types/Items/ShipDecos/MasteryTrophies/Rank${inventory.PlayerLevel.toString().padStart(2, "0")}Trophy`
],
sub: "/Lotus/Language/Inbox/MasteryRewardTitle",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
highPriority: true
}
]);
}
const changedinventory = await inventory.save();

View File

@ -8,8 +8,6 @@ import { buildConfig } from "@/src/services/buildConfigService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { config } from "@/src/services/configService";
import { CRng } from "@/src/services/rngService";
import { ExportRegions } from "warframe-public-export-plus";
export const worldStateController: RequestHandler = (req, res) => {
const worldState: IWorldState = {
@ -20,7 +18,6 @@ export const worldStateController: RequestHandler = (req, res) => {
Time: Math.round(Date.now() / 1000),
Goals: [],
GlobalUpgrades: [],
LiteSorties: [],
EndlessXpChoices: [],
...staticWorldState
};
@ -43,7 +40,7 @@ export const worldStateController: RequestHandler = (req, res) => {
}
const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
const day = Math.trunc((Date.now() - EPOCH) / 86400000);
const day = Math.trunc((new Date().getTime() - EPOCH) / 86400000);
const week = Math.trunc(day / 7);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
@ -52,7 +49,7 @@ export const worldStateController: RequestHandler = (req, res) => {
worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful
// Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
const bountyCycle = Math.trunc(Date.now() / 9000000);
const bountyCycle = Math.trunc(new Date().getTime() / 9000000);
const bountyCycleStart = bountyCycle * 9000000;
const bountyCycleEnd = bountyCycleStart + 9000000;
worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = {
@ -117,67 +114,6 @@ export const worldStateController: RequestHandler = (req, res) => {
});
}
// Archon Hunt cycling every week
{
const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3];
const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3];
const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth
const nodes: string[] = [];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
value.systemIndex === systemIndex &&
value.factionIndex !== undefined &&
value.factionIndex < 2 &&
value.name.indexOf("Archwing") == -1 &&
value.missionIndex != 0 // Exclude MT_ASSASSINATION
) {
nodes.push(key);
}
}
const rng = new CRng(week);
const firstNodeIndex = rng.randomInt(0, nodes.length - 1);
const firstNode = nodes[firstNodeIndex];
nodes.splice(firstNodeIndex, 1);
worldState.LiteSorties.push({
_id: {
$oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c"
},
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards",
Seed: week,
Boss: boss,
Missions: [
{
missionType: rng.randomElement([
"MT_INTEL",
"MT_MOBILE_DEFENSE",
"MT_EXTERMINATION",
"MT_SABOTAGE",
"MT_RESCUE"
]),
node: firstNode
},
{
missionType: rng.randomElement([
"MT_DEFENSE",
"MT_TERRITORY",
"MT_ARTIFACT",
"MT_EXCAVATE",
"MT_SURVIVAL"
]),
node: rng.randomElement(nodes)
},
{
missionType: "MT_ASSASSINATION",
node: showdownNode
}
]
});
}
// Circuit choices cycling every week
worldState.EndlessXpChoices.push({
Category: "EXC_NORMAL",
@ -222,7 +158,7 @@ export const worldStateController: RequestHandler = (req, res) => {
worldState.KnownCalendarSeasons[0].YearIteration = Math.trunc(week / 4);
// Sentient Anomaly cycling every 30 minutes
const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2));
const halfHour = Math.trunc(new Date().getTime() / (unixTimesInMs.hour / 2));
const tmp = {
cavabegin: "1690761600",
PurchasePlatformLockEnabled: true,
@ -261,7 +197,6 @@ interface IWorldState {
Goals: IGoal[];
SyndicateMissions: ISyndicateMission[];
GlobalUpgrades: IGlobalUpgrade[];
LiteSorties: ILiteSortie[];
NodeOverrides: INodeOverride[];
EndlessXpChoices: IEndlessXpChoice[];
KnownCalendarSeasons: ICalendarSeason[];
@ -315,19 +250,6 @@ interface INodeOverride {
CustomNpcEncounters?: string;
}
interface ILiteSortie {
_id: IOid;
Activation: IMongoDate;
Expiry: IMongoDate;
Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards";
Seed: number;
Boss: string; // "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL"
Missions: {
missionType: string;
node: string;
}[];
}
interface IEndlessXpChoice {
Category: string;
Choices: string[];

View File

@ -1,6 +1,8 @@
import { JSONParse } from "../json-with-bigint";
export const getJSONfromString = <T>(str: string): T => {
const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1);
return JSON.parse(jsonSubstring) as T;
return JSONParse(jsonSubstring) as T;
};
export const getSubstringFromKeyword = (str: string, keyword: string): string => {

View File

@ -10,6 +10,21 @@ import { config, validateConfig } from "./services/configService";
import { registerLogFileCreationListener } from "@/src/utils/logger";
import mongoose from "mongoose";
// Patch JSON.stringify to work flawlessly with Bigints. Yeah, it's not pretty.
// TODO: Might wanna use json-with-bigint if/when possible.
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
(BigInt.prototype as any).toJSON = function (): string {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
return "<JS_SUCKS>" + this.toString() + "</JS_SUCKS>";
};
const og_stringify = JSON.stringify;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
(JSON as any).stringify = (obj: any): string => {
return og_stringify(obj).split(`"<JS_SUCKS>`).join(``).split(`</JS_SUCKS>"`).join(``);
};
}
registerLogFileCreationListener();
validateConfig();

53
src/json-with-bigint.ts Normal file
View File

@ -0,0 +1,53 @@
// Based on the json-with-bigint library: https://github.com/Ivan-Korolenko/json-with-bigint/blob/main/json-with-bigint.js
// Sadly we can't use it directly: https://github.com/Ivan-Korolenko/json-with-bigint/issues/15
/* eslint-disable */
const noiseValue = /^-?\d+n+$/; // Noise - strings that match the custom format before being converted to it
export const JSONParse = (json: any) => {
if (!json) return JSON.parse(json);
const MAX_INT = Number.MAX_SAFE_INTEGER.toString();
const MAX_DIGITS = MAX_INT.length;
const stringsOrLargeNumbers =
/"(?:\\.|[^"])*"|-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/g;
const noiseValueWithQuotes = /^"-?\d+n+"$/; // Noise - strings that match the custom format before being converted to it
const customFormat = /^-?\d+n$/;
// Find and mark big numbers with "n"
const serializedData = json.replace(
stringsOrLargeNumbers,
(text: any, digits: any, fractional: any, exponential: any) => {
const isString = text[0] === '"';
const isNoise = isString && Boolean(text.match(noiseValueWithQuotes));
if (isNoise) return text.substring(0, text.length - 1) + 'n"'; // Mark noise values with additional "n" to offset the deletion of one "n" during the processing
const isFractionalOrExponential = fractional || exponential;
const isLessThanMaxSafeInt =
digits &&
(digits.length < MAX_DIGITS ||
(digits.length === MAX_DIGITS && digits <= MAX_INT)); // With a fixed number of digits, we can correctly use lexicographical comparison to do a numeric comparison
if (isString || isFractionalOrExponential || isLessThanMaxSafeInt)
return text;
return '"' + text + 'n"';
}
);
// Convert marked big numbers to BigInt
return JSON.parse(serializedData, (_, value) => {
const isCustomFormatBigInt =
typeof value === "string" && Boolean(value.match(customFormat));
if (isCustomFormatBigInt)
return BigInt(value.substring(0, value.length - 1));
const isNoiseValue =
typeof value === "string" && Boolean(value.match(noiseValue));
if (isNoiseValue) return value.substring(0, value.length - 1); // Remove one "n" off the end of the noisy string
return value;
});
};

View File

@ -40,7 +40,7 @@ export interface IMessage {
export interface Arg {
Key: string;
Tag: string | number;
Tag: string;
}
//types are wrong
@ -99,7 +99,7 @@ const messageSchema = new Schema<IMessageDatabase>(
type: [
{
Key: String,
Tag: Schema.Types.Mixed,
Tag: String,
_id: false
}
],

View File

@ -79,7 +79,10 @@ import {
ICrewShipWeaponDatabase,
IRecentVendorPurchaseDatabase,
IVendorPurchaseHistoryEntryDatabase,
IVendorPurchaseHistoryEntryClient
IVendorPurchaseHistoryEntryClient,
INemesisDatabase,
INemesisClient,
IInfNode
} from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes";
import {
@ -1058,6 +1061,54 @@ const libraryDailyTaskInfoSchema = new Schema<ILibraryDailyTaskInfo>(
{ _id: false }
);
const infNodeSchema = new Schema<IInfNode>(
{
Node: String,
Influence: Number
},
{ _id: false }
);
const nemesisSchema = new Schema<INemesisDatabase>(
{
fp: BigInt,
manifest: String,
KillingSuit: String,
killingDamageType: Number,
ShoulderHelmet: String,
WeaponIdx: Number,
AgentIdx: Number,
BirthNode: String,
Faction: String,
Rank: Number,
k: Boolean,
Traded: Boolean,
d: Date,
PrevOwners: Number,
SecondInCommand: Boolean,
Weakened: Boolean,
InfNodes: [infNodeSchema],
HenchmenKilled: Number,
HintProgress: Number,
Hints: [Number],
GuessHistory: [Number]
},
{ _id: false }
);
nemesisSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
const db = obj as INemesisDatabase;
const client = obj as INemesisClient;
client.d = toMongoDate(db.d);
delete obj._id;
delete obj.__v;
}
});
const alignmentSchema = new Schema<IAlignment>(
{
Alignment: Number,
@ -1341,7 +1392,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//CorpusLich or GrineerLich
NemesisAbandonedRewards: { type: [String], default: [] },
//CorpusLich\KuvaLich
Nemesis: nemesisSchema,
NemesisHistory: [Schema.Types.Mixed],
LastNemesisAllySpawnTime: Schema.Types.Mixed,

View File

@ -69,6 +69,7 @@ import { missionInventoryUpdateController } from "@/src/controllers/api/missionI
import { modularWeaponCraftingController } from "@/src/controllers/api/modularWeaponCraftingController";
import { modularWeaponSaleController } from "@/src/controllers/api/modularWeaponSaleController";
import { nameWeaponController } from "@/src/controllers/api/nameWeaponController";
import { nemesisController } from "@/src/controllers/api/nemesisController";
import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController";
import { playerSkillsController } from "@/src/controllers/api/playerSkillsController";
import { projectionManagerController } from "@/src/controllers/api/projectionManagerController";
@ -203,6 +204,7 @@ apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController);
apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController);
apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController);
apiRouter.post("/nameWeapon.php", nameWeaponController);
apiRouter.post("/nemesis.php", nemesisController);
apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController);
apiRouter.post("/playerSkills.php", playerSkillsController);
apiRouter.post("/projectionManager.php", projectionManagerController);

View File

@ -70,6 +70,7 @@ export const getRandomWeightedRewardUc = <T extends { Rarity: TRarity }>(
return getRandomReward(resultPool);
};
// Seeded RNG for internal usage. Based on recommendations in the ISO C standards.
export class CRng {
state: number;
@ -92,3 +93,21 @@ export class CRng {
return arr[Math.floor(this.random() * arr.length)];
}
}
// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.
export class SRng {
state: bigint;
constructor(seed: bigint) {
this.state = seed;
}
randomInt(min: number, max: number): number {
const diff = max - min;
if (diff != 0) {
this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
min += (Number(this.state >> 32n) & 0x3fffffff) % (diff + 1);
}
return min;
}
}

View File

@ -14,7 +14,6 @@ import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorIn
import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json";
import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json";
import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json";
import GuildAdvertisementVendorManifest from "@/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json";
import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json";
import HubsPerrinSequenceWeaponVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json";
import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json";
@ -64,7 +63,6 @@ const vendorManifests: IVendorManifest[] = [
DuviriAcrithisVendorManifest,
EntratiLabsEntratiLabsCommisionsManifest,
EntratiLabsEntratiLabVendorManifest,
GuildAdvertisementVendorManifest,
HubsIronwakeDondaVendorManifest,
HubsPerrinSequenceWeaponVendorManifest,
HubsRailjackCrewMemberVendorManifest,

View File

@ -43,6 +43,7 @@ export interface IInventoryDatabase
| "Drones"
| "RecentVendorPurchases"
| "NextRefill"
| "Nemesis"
| TEquipmentKey
>,
InventoryDatabaseEquipment {
@ -71,6 +72,7 @@ export interface IInventoryDatabase
Drones: IDroneDatabase[];
RecentVendorPurchases?: IRecentVendorPurchaseDatabase[];
NextRefill?: Date;
Nemesis?: INemesisDatabase;
}
export interface IQuestKeyDatabase {
@ -288,7 +290,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
SeasonChallengeHistory: ISeasonChallenge[];
EquippedInstrument?: string;
InvasionChainProgress: IInvasionChainProgress[];
NemesisHistory: INemesisHistory[];
Nemesis?: INemesisClient;
NemesisHistory: INemesisBaseClient[];
LastNemesisAllySpawnTime?: IMongoDate;
Settings: ISettings;
PersonalTechProjects: IPersonalTechProject[];
@ -782,38 +785,44 @@ export interface IMission extends IMissionDatabase {
RewardsCooldownTime?: IMongoDate;
}
export interface INemesisHistory {
fp: number;
manifest: Manifest;
export interface INemesisBaseClient {
fp: bigint;
manifest: string;
KillingSuit: string;
killingDamageType: number;
ShoulderHelmet: string;
WeaponIdx: number;
AgentIdx: number;
BirthNode: BirthNode;
BirthNode: string;
Faction: string;
Rank: number;
k: boolean;
Traded: boolean;
d: IMongoDate;
GuessHistory?: number[];
currentGuess?: number;
Traded?: boolean;
PrevOwners?: number;
SecondInCommand?: boolean;
Faction?: string;
Weakened?: boolean;
PrevOwners: number;
SecondInCommand: boolean;
Weakened: boolean;
}
export enum BirthNode {
SolNode181 = "SolNode181",
SolNode4 = "SolNode4",
SolNode70 = "SolNode70",
SolNode76 = "SolNode76"
export interface INemesisBaseDatabase extends Omit<INemesisBaseClient, "d"> {
d: Date;
}
export enum Manifest {
LotusTypesEnemiesCorpusLawyersLawyerManifest = "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifest",
LotusTypesGameNemesisKuvaLichKuvaLichManifest = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifest",
LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionThree = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionThree",
LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionTwo = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionTwo"
export interface INemesisClient extends INemesisBaseClient {
InfNodes: IInfNode[];
HenchmenKilled: number;
HintProgress: number;
Hints: number[];
GuessHistory: number[];
}
export interface INemesisDatabase extends Omit<INemesisClient, "d"> {
d: Date;
}
export interface IInfNode {
Node: string;
Influence: number;
}
export interface IPendingCouponDatabase {

View File

@ -1,71 +0,0 @@
{
"VendorInfo": {
"_id": { "$oid": "61ba123467e5d37975aeeb03" },
"TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMoon",
"ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 12, "ProductCategory": "MiscItems" }],
"RegularPrice": [1, 1],
"Bin": "BIN_4",
"QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "9999999000000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 79554843,
"Id": { "$oid": "67bbb592e1534511d6c1c1e2" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMountain",
"ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 7, "ProductCategory": "MiscItems" }],
"RegularPrice": [1, 1],
"Bin": "BIN_3",
"QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "9999999000000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 2413820225,
"Id": { "$oid": "67bbb592e1534511d6c1c1e3" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementStorm",
"ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/ChemComponent", "ItemCount": 3, "ProductCategory": "MiscItems" }],
"RegularPrice": [1, 1],
"Bin": "BIN_2",
"QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "9999999000000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 3262300883,
"Id": { "$oid": "67bbb592e1534511d6c1c1e4" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementShadow",
"ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/EnergyFragment", "ItemCount": 20, "ProductCategory": "MiscItems" }],
"RegularPrice": [1, 1],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "9999999000000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 2797325750,
"Id": { "$oid": "67bbb592e1534511d6c1c1e5" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementGhost",
"ItemPrices": [{ "ItemType": "/Lotus/Types/Items/Research/EnergyFragment", "ItemCount": 10, "ProductCategory": "MiscItems" }],
"RegularPrice": [1, 1],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "9999999000000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 554932310,
"Id": { "$oid": "67bbb592e1534511d6c1c1e6" }
}
],
"PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA",
"RandomSeedType": "VRST_FLAVOUR_TEXT",
"Expiry": { "$date": { "$numberLong": "9999999000000" } }
}
}

View File

@ -79,6 +79,21 @@
"Twitter": true
}
],
"LiteSorties": [
{
"_id": { "$oid": "663819fd1cec9ebe9d83a06e" },
"Activation": { "$date": { "$numberLong": "1714953600000" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Reward": "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards",
"Seed": 58034,
"Boss": "SORTIE_BOSS_NIRA",
"Missions": [
{ "missionType": "MT_MOBILE_DEFENSE", "node": "SolNode125" },
{ "missionType": "MT_SURVIVAL", "node": "SolNode74" },
{ "missionType": "MT_ASSASSINATION", "node": "SolNode53" }
]
}
],
"SyndicateMissions": [
{
"_id": { "$oid": "663a4fc5ba6f84724fa48049" },

View File

@ -373,7 +373,7 @@
<option value="LotusShotgunRandomModRare">LotusShotgunRandomModRare</option>
<option value="PlayerMeleeWeaponRandomModRare">PlayerMeleeWeaponRandomModRare</option>
</select>
<textarea id="addriven-fingerprint" class="form-control mb-3" data-loc-placeholder="mods_fingerprint"></textarea>
<textarea id="addriven-fingerprint" class="form-control mb-3" data-loc-placeholder_"mods.fingerprint"></textarea>
<button class="btn btn-primary" style="margin-right: 5px" type="submit" data-loc="general_addButton"></button>
<a href="riven-tool/" target="_blank" data-loc="mods_fingerprintHelp"></a>
</form>

View File

@ -118,9 +118,6 @@ function updateLocElements() {
document.querySelectorAll("[data-loc]").forEach(elm => {
elm.innerHTML = loc(elm.getAttribute("data-loc"));
});
document.querySelectorAll("[data-loc-placeholder]").forEach(elm => {
elm.placeholder = loc(elm.getAttribute("data-loc-placeholder"));
});
}
function setActiveLanguage(lang) {