Compare commits

...

19 Commits

Author SHA1 Message Date
a9f1368cb7 fix: add UpgradeType field to repaired railjack weapons (#2193)
Reviewed-on: OpenWF/SpaceNinjaServer#2193
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 20:58:10 -07:00
cccf6f04a5 feat: xtra cheese rotation (#2191)
This has a bug where the client shows a negative time for "Xtra cheese starts in ..." until it refreshes the world state. This is because we're only providing the new activation as soon as that time/date is reached. However, this is 100% faithful to live.

Reviewed-on: OpenWF/SpaceNinjaServer#2191
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 17:32:34 -07:00
1ead581780 chore: improve typing of IFocusXp (#2182)
Any given focus school can be undefined in this object due to importing.

Reviewed-on: OpenWF/SpaceNinjaServer#2182
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 05:02:58 -07:00
145d21e30e fix: weaken infested lich (#2181)
Closes #2180

Reviewed-on: OpenWF/SpaceNinjaServer#2181
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 05:02:34 -07:00
6c2055a246 feat: echoes of umbra (#2177)
Having this item in the inventory unlocks the helminth option which is helpfully called "remove cyst" to install and uninstall it on a frame.

Reviewed-on: OpenWF/SpaceNinjaServer#2177
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 05:02:11 -07:00
01e490768c fix: ensure helminth shard operations don't produce a null shard (#2176)
Reviewed-on: OpenWF/SpaceNinjaServer#2176
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 05:01:55 -07:00
2e8fe799d7 feat: setSuitInfection (#2174)
Closes #2172

Reviewed-on: OpenWF/SpaceNinjaServer#2174
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-17 05:01:21 -07:00
53976378bb chore(webui): update Chinese translation (#2184)
Reviewed-on: OpenWF/SpaceNinjaServer#2184
Co-authored-by: qianlishun <qianlishun@noreply.localhost>
Co-committed-by: qianlishun <qianlishun@noreply.localhost>
2025-06-17 03:28:49 -07:00
4e832d3b2c chore(webui): add login/register error messages to translation system (#2179)
Closes #2178

Reviewed-on: OpenWF/SpaceNinjaServer#2179
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-16 15:05:26 -07:00
8c1147998d fix(webui): respond with 200 for successful shard operations (#2175)
Reviewed-on: OpenWF/SpaceNinjaServer#2175
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-16 14:55:56 -07:00
3e99e069be feat: void storm rotation (#2171)
Re #1512

Reviewed-on: OpenWF/SpaceNinjaServer#2171
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-16 14:55:35 -07:00
9731004de6 feat: autogenerate railjack crew member vendor (#2170)
Reviewed-on: OpenWF/SpaceNinjaServer#2170
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-16 14:51:47 -07:00
b98a88b700 chore: retroactively populate vendor to hide that it was just generated (#2168)
For example, debt-bonds at ticker always expire in at least 2 hours so visiting him, you'd never see an offer with an expiry less than that. The solution here is simply generating offers for the last few hours.

Reviewed-on: OpenWF/SpaceNinjaServer#2168
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-15 18:52:10 -07:00
6023f1c113 feat: autogenerate "today's special" at mining vendors (#2167)
Reviewed-on: OpenWF/SpaceNinjaServer#2167
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-15 18:51:42 -07:00
c6dd8bfb81 chore: improve error reporting when config.json exists with invalid json (#2166)
Reviewed-on: OpenWF/SpaceNinjaServer#2166
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-15 18:51:32 -07:00
3053112428 chore: auto-generate palladino's vendor manifest (#2160)
A bit ugly, but having the self test forces correctness.

Reviewed-on: OpenWF/SpaceNinjaServer#2160
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-15 05:53:10 -07:00
f448d03880 fix: 1999 bounty chemistry (#2164)
Closes #2162

Reviewed-on: OpenWF/SpaceNinjaServer#2164
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-15 05:05:35 -07:00
d00fbed46f fix: treating chemstry delta as absolute value (#2163)
Reviewed-on: OpenWF/SpaceNinjaServer#2163
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-06-15 05:05:18 -07:00
c283d61399 chore(webui): update to Spanish translation (#2165)
Reviewed-on: OpenWF/SpaceNinjaServer#2165
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-06-14 23:44:34 -07:00
35 changed files with 485 additions and 826 deletions

8
package-lock.json generated
View File

@ -18,7 +18,7 @@
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.66",
"warframe-public-export-plus": "^0.5.67",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
@ -3814,9 +3814,9 @@
}
},
"node_modules/warframe-public-export-plus": {
"version": "0.5.66",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.66.tgz",
"integrity": "sha512-AU7XQA96OfYrLm2RioCwDjjdI3IrsmUiqebXyE+bpM0iST+4x/NHu8LTRT4Oygfo/2OBtDYhib7G6re0EeAe5g=="
"version": "0.5.67",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.67.tgz",
"integrity": "sha512-LsnZD2E5PTA+5MK9kDGvM/hFDtg8sb0EwQ4hKH5ILqrSgz30a9W8785v77RSsL1AEVF8dfb/lZcSTCJq1DZHzQ=="
},
"node_modules/warframe-riven-info": {
"version": "0.1.2",

View File

@ -25,7 +25,7 @@
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.66",
"warframe-public-export-plus": "^0.5.67",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"

View File

@ -43,7 +43,7 @@ export const focusController: RequestHandler = async (req, res) => {
inventory.FocusAbility ??= focusType;
inventory.FocusUpgrades.push({ ItemType: focusType });
if (inventory.FocusXP) {
inventory.FocusXP[focusPolarity] -= cost;
inventory.FocusXP[focusPolarity]! -= cost;
}
await inventory.save();
res.json({
@ -78,7 +78,7 @@ export const focusController: RequestHandler = async (req, res) => {
cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
}
inventory.FocusXP![focusPolarity] -= cost;
inventory.FocusXP![focusPolarity]! -= cost;
await inventory.save();
res.json({
FocusTypes: request.FocusTypes,
@ -96,7 +96,7 @@ export const focusController: RequestHandler = async (req, res) => {
const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
focusUpgradeDb.Level = focusUpgrade.Level;
}
inventory.FocusXP![focusPolarity] -= cost;
inventory.FocusXP![focusPolarity]! -= cost;
await inventory.save();
res.json({
FocusInfos: request.FocusInfos,
@ -123,7 +123,7 @@ export const focusController: RequestHandler = async (req, res) => {
const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
const inventory = await getInventory(accountId);
inventory.FocusXP![focusPolarity] -= 750_000 * request.FocusTypes.length;
inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length;
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
@ -168,8 +168,10 @@ export const focusController: RequestHandler = async (req, res) => {
shard.ItemCount *= -1;
}
const inventory = await getInventory(accountId);
inventory.FocusXP ??= { AP_POWER: 0, AP_TACTIC: 0, AP_DEFENSE: 0, AP_ATTACK: 0, AP_WARD: 0 };
inventory.FocusXP[request.Polarity] += xp;
const polarity = request.Polarity;
inventory.FocusXP ??= {};
inventory.FocusXP[polarity] ??= 0;
inventory.FocusXP[polarity] += xp;
addMiscItems(inventory, request.Shards);
await inventory.save();
break;

View File

@ -11,7 +11,7 @@ import {
scaleRequiredCount,
setGuildTechLogState
} from "@/src/services/guildService";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { ExportDojoRecipes, ExportRailjackWeapons } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService";
import {
addCrewShipWeaponSkin,
@ -442,6 +442,7 @@ const finishComponentRepair = (
...(category == "CrewShipWeaponSkins"
? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
: addEquipment(inventory, category, salvageItem.ItemType, {
UpgradeType: ExportRailjackWeapons[salvageItem.ItemType].defaultUpgrades?.[0].ItemType,
UpgradeFingerprint: salvageItem.UpgradeFingerprint
})),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)

View File

@ -30,8 +30,9 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(account._id.toString());
const suit = inventory.Suits.id(request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
suit.ArchonCrystalUpgrades ??= [];
while (suit.ArchonCrystalUpgrades.length < request.Slot) {
suit.ArchonCrystalUpgrades.push({});
}
suit.ArchonCrystalUpgrades[request.Slot] = {
UpgradeType: request.UpgradeType,
@ -92,7 +93,8 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
}
// remove from suit
suit.ArchonCrystalUpgrades![request.Slot] = {};
suit.ArchonCrystalUpgrades![request.Slot].UpgradeType = undefined;
suit.ArchonCrystalUpgrades![request.Slot].Color = undefined;
await inventory.save();

View File

@ -1,5 +1,6 @@
import { version_compare } from "@/src/helpers/inventoryHelpers";
import {
antivirusMods,
consumeModCharge,
decodeNemesisGuess,
encodeNemesisGuess,
@ -134,34 +135,37 @@ export const nemesisController: RequestHandler = async (req, res) => {
for (const upgrade of body.knife!.AttachedUpgrades) {
switch (upgrade.ItemType) {
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
}
}
inventory.Nemesis!.HenchmenKilled += antivirusGain;
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
// Client doesn't seem to request mode=w for infested liches, so weakening it here.
inventory.Nemesis!.InfNodes = [
{
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, antivirusMods[passcode]);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
}
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
if (inventory.Nemesis!.HenchmenKilled < 100) {
inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0);
}
inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0);
await inventory.save();
res.json(response);
@ -283,6 +287,10 @@ export const nemesisController: RequestHandler = async (req, res) => {
);
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
if (inventory.Nemesis!.Weakened) {
logger.warn(`client is weakening an already-weakened nemesis?!`);
}
inventory.Nemesis!.InfNodes = [
{
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,

View File

@ -24,7 +24,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
inventory.DialogueHistory.Dialogues ??= [];
const dialogue = getDialogue(inventory, request.DialogueName);
dialogue.Rank = request.Rank;
dialogue.Chemistry = request.Chemistry;
dialogue.Chemistry += request.Chemistry;
dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) {
dialogue.Booleans.push(bool);

View File

@ -0,0 +1,22 @@
import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { RequestHandler } from "express";
export const setSuitInfectionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Suits");
const payload = getJSONfromString<ISetSuitInfectionRequest>(String(req.body));
for (const clientSuit of payload.Suits) {
const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!;
dbSuit.InfestationDate = fromMongoDate(clientSuit.InfestationDate!);
}
await inventory.save();
res.end();
};
interface ISetSuitInfectionRequest {
Suits: IEquipmentClient[];
}

View File

@ -0,0 +1,27 @@
import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItem, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { RequestHandler } from "express";
export const umbraController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Suits MiscItems");
const payload = getJSONfromString<IUmbraRequest>(String(req.body));
for (const clientSuit of payload.Suits) {
const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!;
if (clientSuit.UmbraDate) {
addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/UmbraEchoes", -1);
dbSuit.UmbraDate = fromMongoDate(clientSuit.UmbraDate);
} else {
dbSuit.UmbraDate = undefined;
}
}
await inventory.save();
res.end();
};
interface IUmbraRequest {
Suits: IEquipmentClient[];
}

View File

@ -12,6 +12,7 @@ export const popArchonCrystalUpgradeController: RequestHandler = async (req, res
);
await inventory.save();
res.end();
return;
}
res.status(400).end();
};

View File

@ -15,6 +15,7 @@ export const pushArchonCrystalUpgradeController: RequestHandler = async (req, re
}
await inventory.save();
res.end();
return;
}
}
res.status(400).end();

View File

@ -248,7 +248,7 @@ const requiemMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod"
];
const antivirusMods: readonly string[] = [
export const antivirusMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",

View File

@ -1,9 +1,14 @@
// First, init config.
import { config, loadConfig } from "@/src/services/configService";
import fs from "fs";
try {
loadConfig();
} catch (e) {
console.log("ERROR: Failed to load config.json. You can copy config.json.example to create your config.json.");
if (fs.existsSync("config.json")) {
console.log("Failed to load config.json: " + (e as Error).message);
} else {
console.log("Failed to load config.json. You can copy config.json.example to create your config.json.");
}
process.exit(1);
}

View File

@ -251,12 +251,6 @@ const ArchonCrystalUpgradeSchema = new Schema<IArchonCrystalUpgrade>(
{ _id: false }
);
ArchonCrystalUpgradeSchema.set("toJSON", {
transform(_document, returnedObject) {
delete returnedObject.__v;
}
});
const boosterSchema = new Schema<IBooster>(
{
ExpiryDate: Number,
@ -1079,6 +1073,11 @@ EquipmentSchema.set("toJSON", {
if (db.UmbraDate) {
client.UmbraDate = toMongoDate(db.UmbraDate);
}
if (client.ArchonCrystalUpgrades) {
// For some reason, mongoose turns empty objects here into nulls, so we have to fix it.
client.ArchonCrystalUpgrades = client.ArchonCrystalUpgrades.map(x => (x as unknown) ?? {});
}
}
});

View File

@ -132,6 +132,7 @@ import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDeco
import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController";
import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController";
import { setShipVignetteController } from "@/src/controllers/api/setShipVignetteController";
import { setSuitInfectionController } from "@/src/controllers/api/setSuitInfectionController";
import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController";
import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController";
import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController";
@ -147,6 +148,7 @@ import { syndicateStandingBonusController } from "@/src/controllers/api/syndicat
import { tauntHistoryController } from "@/src/controllers/api/tauntHistoryController";
import { tradingController } from "@/src/controllers/api/tradingController";
import { trainingResultController } from "@/src/controllers/api/trainingResultController";
import { umbraController } from "@/src/controllers/api/umbraController";
import { unlockShipFeatureController } from "@/src/controllers/api/unlockShipFeatureController";
import { updateAlignmentController } from "@/src/controllers/api/updateAlignmentController";
import { updateChallengeProgressController } from "@/src/controllers/api/updateChallengeProgressController";
@ -317,6 +319,7 @@ apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController);
apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController);
apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController);
apiRouter.post("/setShipVignette.php", setShipVignetteController);
apiRouter.post("/setSuitInfection.php", setSuitInfectionController);
apiRouter.post("/setWeaponSkillTree.php", setWeaponSkillTreeController);
apiRouter.post("/shipDecorations.php", shipDecorationsController);
apiRouter.post("/startCollectibleEntry.php", startCollectibleEntryController);
@ -327,6 +330,7 @@ apiRouter.post("/syndicateSacrifice.php", syndicateSacrificeController);
apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController);
apiRouter.post("/tauntHistory.php", tauntHistoryController);
apiRouter.post("/trainingResult.php", trainingResultController);
apiRouter.post("/umbra.php", umbraController);
apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController);
apiRouter.post("/updateAlignment.php", updateAlignmentController);
apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController);

View File

@ -13,7 +13,7 @@ fs.watchFile(configPath, () => {
try {
loadConfig();
} catch (e) {
logger.error("Failed to reload config.json. Did you delete it?! Execution cannot continue.");
logger.error("FATAL ERROR: Config failed to be reloaded: " + (e as Error).message);
process.exit(1);
}
validateConfig();

View File

@ -1580,7 +1580,7 @@ export const addMiscItem = (
inventory: TInventoryDatabaseDocument,
type: string,
count: number,
inventoryChanges: IInventoryChanges
inventoryChanges: IInventoryChanges = {}
): void => {
const miscItemChanges: IMiscItem[] = [
{
@ -1731,12 +1731,27 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus
AP_ANY
}
inventory.FocusXP ??= { AP_ATTACK: 0, AP_DEFENSE: 0, AP_TACTIC: 0, AP_POWER: 0, AP_WARD: 0 };
inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK];
inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE];
inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC];
inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER];
inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD];
inventory.FocusXP ??= {};
if (focusXpPlus[FocusType.AP_ATTACK]) {
inventory.FocusXP.AP_ATTACK ??= 0;
inventory.FocusXP.AP_ATTACK += focusXpPlus[FocusType.AP_ATTACK];
}
if (focusXpPlus[FocusType.AP_DEFENSE]) {
inventory.FocusXP.AP_DEFENSE ??= 0;
inventory.FocusXP.AP_DEFENSE += focusXpPlus[FocusType.AP_DEFENSE];
}
if (focusXpPlus[FocusType.AP_TACTIC]) {
inventory.FocusXP.AP_TACTIC ??= 0;
inventory.FocusXP.AP_TACTIC += focusXpPlus[FocusType.AP_TACTIC];
}
if (focusXpPlus[FocusType.AP_POWER]) {
inventory.FocusXP.AP_POWER ??= 0;
inventory.FocusXP.AP_POWER += focusXpPlus[FocusType.AP_POWER];
}
if (focusXpPlus[FocusType.AP_WARD]) {
inventory.FocusXP.AP_WARD ??= 0;
inventory.FocusXP.AP_WARD += focusXpPlus[FocusType.AP_WARD];
}
if (!config.noDailyFocusLimit) {
inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0);

View File

@ -66,15 +66,7 @@ import {
} from "@/src/helpers/nemesisHelpers";
import { Loadout } from "../models/inventoryModels/loadoutModel";
import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes";
import {
getLiteSortie,
getSortie,
getWorldState,
idToBountyCycle,
idToDay,
idToWeek,
pushClassicBounties
} from "./worldStateService";
import { getLiteSortie, getSortie, idToBountyCycle, idToDay, idToWeek, pushClassicBounties } from "./worldStateService";
import { config } from "./configService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { ISyndicateMissionInfo } from "../types/worldStateTypes";
@ -1266,9 +1258,9 @@ export const addMissionRewards = async (
}
if (rewardInfo.challengeMissionId) {
const [syndicateTag, tierStr, chemistryStr] = rewardInfo.challengeMissionId.split("_");
const [syndicateTag, tierStr, chemistryBuddyStr] = rewardInfo.challengeMissionId.split("_");
const tier = Number(tierStr);
const chemistry = Number(chemistryStr);
const chemistryBuddy = Number(chemistryBuddyStr);
const isSteelPath = missions?.Tier;
if (syndicateTag === "ZarimanSyndicate") {
let medallionAmount = tier + 1;
@ -1285,22 +1277,19 @@ export const addMissionRewards = async (
if (isSteelPath) standingAmount *= 1.5;
addStanding(inventory, syndicateTag, standingAmount, AffiliationMods);
}
if (syndicateTag == "HexSyndicate" && chemistry && tier < 6) {
const seed = getWorldState().SyndicateMissions.find(x => x.Tag == "HexSyndicate")!.Seed;
const { nodes, buddies } = getHexBounties(seed);
const buddy = buddies[tier];
logger.debug(`Hex seed is ${seed}, giving chemistry for ${buddy}`);
if (missions?.Tag != nodes[tier]) {
logger.warn(
`Uh-oh, tier ${tier} bounty should've been on ${nodes[tier]} but you were just on ${missions?.Tag}`
);
}
const tomorrowAt0Utc = config.noKimCooldowns
? Date.now()
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
if (syndicateTag == "HexSyndicate" && tier < 6) {
const buddy = chemistryBuddies[chemistryBuddy];
const dialogue = getDialogue(inventory, buddy);
dialogue.Chemistry += chemistry;
dialogue.BountyChemExpiry = new Date(tomorrowAt0Utc);
if (Date.now() >= dialogue.BountyChemExpiry.getTime()) {
logger.debug(`Giving 20 chemistry for ${buddy}`);
const tomorrowAt0Utc = config.noKimCooldowns
? Date.now()
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
dialogue.Chemistry += 20;
dialogue.BountyChemExpiry = new Date(tomorrowAt0Utc);
} else {
logger.debug(`Already got today's chemistry for ${buddy}`);
}
}
if (isSteelPath) {
await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1);
@ -1864,7 +1853,16 @@ const libraryPersonalTargetToAvatar: Record<string, string> = {
"/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar"
};
const node_excluded_buddies: Record<string, string> = {
const chemistryBuddies: readonly string[] = [
"/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
"/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue"
];
/*const node_excluded_buddies: Record<string, string> = {
SolNode856: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
SolNode852: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
SolNode851: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
@ -1914,4 +1912,4 @@ const getHexBounties = (seed: number): { nodes: string[]; buddies: string[] } =>
}
}
return { nodes, buddies };
};
};*/

View File

@ -6,7 +6,7 @@ import { mixSeeds, SRng } from "@/src/services/rngService";
import { IMongoDate } from "@/src/types/commonTypes";
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
import { logger } from "@/src/utils/logger";
import { ExportVendors, IRange, IVendor } from "warframe-public-export-plus";
import { ExportVendors, IRange, IVendor, IVendorOffer } from "warframe-public-export-plus";
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
@ -17,18 +17,13 @@ import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_response
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 HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json";
import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json";
import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json";
import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json";
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json";
import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json";
import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json";
import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json";
@ -43,18 +38,13 @@ const rawVendorManifests: IVendorManifest[] = [
DeimosHivemindCommisionsManifestWeaponsmith,
DeimosHivemindTokenVendorManifest,
DeimosPetVendorManifest,
DeimosProspectorVendorManifest,
DuviriAcrithisVendorManifest,
EntratiLabsEntratiLabsCommisionsManifest,
EntratiLabsEntratiLabVendorManifest,
HubsIronwakeDondaVendorManifest, // uses preprocessing
HubsRailjackCrewMemberVendorManifest,
MaskSalesmanManifest,
Nova1999ConquestShopManifest,
OstronPetVendorManifest,
OstronProspectorVendorManifest,
SolarisDebtTokenVendorRepossessionsManifest,
SolarisProspectorVendorManifest,
Temple1999VendorManifest,
TeshinHardModeVendorManifest, // uses preprocessing
ZarimanCommisionsManifestArchimedean
@ -83,10 +73,6 @@ const generatableVendors: IGeneratableVendorInfo[] = [
cycleOffset: 1744934400_000,
cycleDuration: 4 * unixTimesInMs.day
}
// {
// _id: { $oid: "5dbb4c41e966f7886c3ce939" },
// TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest"
// }
];
const getVendorOid = (typeName: string): string => {
@ -228,6 +214,22 @@ const toRange = (value: IRange | number): IRange => {
return value;
};
const getCycleDurationRange = (manifest: IVendor): IRange | undefined => {
const res: IRange = { minValue: Number.MAX_SAFE_INTEGER, maxValue: 0 };
for (const offer of manifest.items) {
if (offer.durationHours) {
const range = toRange(offer.durationHours);
if (res.minValue > range.minValue) {
res.minValue = range.minValue;
}
if (res.maxValue < range.maxValue) {
res.maxValue = range.maxValue;
}
}
}
return res.maxValue != 0 ? res : undefined;
};
const vendorManifestCache: Record<string, IVendorManifest> = {};
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
@ -244,10 +246,16 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
}
const cacheEntry = vendorManifestCache[vendorInfo.TypeName];
const info = cacheEntry.VendorInfo;
if (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) {
const manifest = ExportVendors[vendorInfo.TypeName];
const cycleDurationRange = getCycleDurationRange(manifest);
let now = Date.now();
if (cycleDurationRange && cycleDurationRange.minValue != cycleDurationRange.maxValue) {
now -= (cycleDurationRange.maxValue - 1) * unixTimesInMs.hour;
}
while (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) {
// Remove expired offers
for (let i = 0; i != info.ItemManifest.length; ) {
if (Date.now() >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) {
if (now >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) {
info.ItemManifest.splice(i, 1);
} else {
++i;
@ -258,48 +266,78 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16);
const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000;
const cycleDuration = vendorInfo.cycleDuration;
const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration);
const cycleIndex = Math.trunc((now - cycleOffset) / cycleDuration);
const rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
const manifest = ExportVendors[vendorInfo.TypeName];
const offersToAdd = [];
if (
manifest.numItems &&
(manifest.numItems.minValue != manifest.numItems.maxValue ||
manifest.items.length != manifest.numItems.minValue) &&
!manifest.isOneBinPerCycle
) {
const offersToAdd: IVendorOffer[] = [];
if (!manifest.isOneBinPerCycle) {
const remainingItemCapacity: Record<string, number> = {};
const missingItemsPerBin: Record<number, number> = {};
let numOffersThatNeedToMatchABin = 0;
if (manifest.numItemsPerBin) {
for (let bin = 0; bin != manifest.numItemsPerBin.length; ++bin) {
missingItemsPerBin[bin] = manifest.numItemsPerBin[bin];
numOffersThatNeedToMatchABin += manifest.numItemsPerBin[bin];
}
}
for (const item of manifest.items) {
remainingItemCapacity[item.storeItem] = 1 + item.duplicates;
}
for (const offer of info.ItemManifest) {
remainingItemCapacity[offer.StoreItem] -= 1;
}
const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) {
// TODO: Consider per-bin item limits
// TODO: Consider item probability weightings
const item = rng.randomElement(manifest.items)!;
if (remainingItemCapacity[item.storeItem] != 0) {
remainingItemCapacity[item.storeItem] -= 1;
offersToAdd.push(item);
const bin = parseInt(offer.Bin.substring(4));
if (missingItemsPerBin[bin]) {
missingItemsPerBin[bin] -= 1;
numOffersThatNeedToMatchABin -= 1;
}
}
if (manifest.numItems && manifest.items.length != manifest.numItems.minValue) {
const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) {
// TODO: Consider item probability weightings
const item = rng.randomElement(manifest.items)!;
if (
remainingItemCapacity[item.storeItem] != 0 &&
(numOffersThatNeedToMatchABin == 0 || missingItemsPerBin[item.bin])
) {
remainingItemCapacity[item.storeItem] -= 1;
if (missingItemsPerBin[item.bin]) {
missingItemsPerBin[item.bin] -= 1;
numOffersThatNeedToMatchABin -= 1;
}
offersToAdd.push(item);
}
}
} else {
for (const item of manifest.items) {
if (!item.alwaysOffered && remainingItemCapacity[item.storeItem] != 0) {
remainingItemCapacity[item.storeItem] -= 1;
offersToAdd.push(item);
}
}
for (const e of Object.entries(remainingItemCapacity)) {
const item = manifest.items.find(x => x.storeItem == e[0])!;
if (!item.alwaysOffered) {
while (e[1] != 0) {
e[1] -= 1;
offersToAdd.push(item);
}
}
}
for (const item of manifest.items) {
if (item.alwaysOffered && remainingItemCapacity[item.storeItem] != 0) {
remainingItemCapacity[item.storeItem] -= 1;
offersToAdd.push(item);
}
}
offersToAdd.reverse();
}
} else {
let binThisCycle;
if (manifest.isOneBinPerCycle) {
binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
}
const binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
for (const rawItem of manifest.items) {
if (!manifest.isOneBinPerCycle || rawItem.bin == binThisCycle) {
if (rawItem.bin == binThisCycle) {
offersToAdd.push(rawItem);
}
}
// For most vendors, the offers seem to roughly be in reverse order from the manifest. Coda weapons are an odd exception.
if (!manifest.isOneBinPerCycle) {
offersToAdd.reverse();
}
}
const cycleStart = cycleOffset + cycleIndex * cycleDuration;
for (const rawItem of offersToAdd) {
@ -311,7 +349,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
StoreItem: rawItem.storeItem,
ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
Bin: "BIN_" + rawItem.bin,
QuantityMultiplier: 1,
QuantityMultiplier: rawItem.quantity,
Expiry: { $date: { $numberLong: expiry.toString() } },
AllowMultipurchase: false,
Id: {
@ -362,6 +400,12 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
info.ItemManifest.push(item);
}
info.ItemManifest.sort((a, b) => {
const aBin = parseInt(a.Bin.substring(4));
const bBin = parseInt(b.Bin.substring(4));
return aBin == bBin ? 0 : aBin < bBin ? +1 : -1;
});
// Update vendor expiry
let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
for (const offer of info.ItemManifest) {
@ -371,6 +415,8 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
}
}
info.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
now += unixTimesInMs.hour;
}
return cacheEntry;
};
@ -388,4 +434,30 @@ if (isDev) {
) {
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest`);
}
const pall = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest")!
.VendorInfo.ItemManifest;
if (
pall.length != 5 ||
pall[0].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/HarrowQuestKeyOrnament" ||
pall[1].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack" ||
pall[2].StoreItem != "/Lotus/StoreItems/Types/StoreItems/CreditBundles/150000Credits" ||
pall[3].StoreItem != "/Lotus/StoreItems/Types/Items/MiscItems/Kuva" ||
pall[4].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack"
) {
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest`);
}
const cms = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest")!
.VendorInfo.ItemManifest;
if (
cms.length != 9 ||
cms[0].Bin != "BIN_2" ||
cms[8].Bin != "BIN_0" ||
cms.reduce((a, x) => a + (x.Bin == "BIN_2" ? 1 : 0), 0) < 2 ||
cms.reduce((a, x) => a + (x.Bin == "BIN_1" ? 1 : 0), 0) < 2 ||
cms.reduce((a, x) => a + (x.Bin == "BIN_0" ? 1 : 0), 0) < 4
) {
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest`);
}
}

View File

@ -16,6 +16,7 @@ import {
ISortie,
ISortieMission,
ISyndicateMissionInfo,
IVoidStorm,
IWorldState
} from "../types/worldStateTypes";
import { version_compare } from "../helpers/inventoryHelpers";
@ -963,6 +964,61 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
};
};
// Not very faithful, but to avoid the same node coming up back-to-back (which is not valid), I've split these into 2 arrays which we're alternating between.
const voidStormMissionsA = {
VoidT1: ["CrewBattleNode519", "CrewBattleNode518", "CrewBattleNode515", "CrewBattleNode503"],
VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530"],
VoidT3: ["CrewBattleNode521", "CrewBattleNode516"],
VoidT4: [
"CrewBattleNode555",
"CrewBattleNode553",
"CrewBattleNode554",
"CrewBattleNode539",
"CrewBattleNode531",
"CrewBattleNode527"
]
};
const voidStormMissionsB = {
VoidT1: ["CrewBattleNode509", "CrewBattleNode522", "CrewBattleNode511", "CrewBattleNode512"],
VoidT2: ["CrewBattleNode535", "CrewBattleNode533"],
VoidT3: ["CrewBattleNode524", "CrewBattleNode525"],
VoidT4: [
"CrewBattleNode542",
"CrewBattleNode538",
"CrewBattleNode543",
"CrewBattleNode536",
"CrewBattleNode550",
"CrewBattleNode529"
]
};
const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => {
const activation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
const expiry = activation + 90 * unixTimesInMs.minute;
let accum = 0;
const rng = new SRng(new SRng(hour).randomInt(0, 100_000));
const voidStormMissions = structuredClone(hour & 1 ? voidStormMissionsA : voidStormMissionsB);
for (const tier of ["VoidT1", "VoidT1", "VoidT2", "VoidT3", "VoidT4", "VoidT4"] as const) {
const idx = rng.randomInt(0, voidStormMissions[tier].length - 1);
const node = voidStormMissions[tier][idx];
voidStormMissions[tier].splice(idx, 1);
arr.push({
_id: {
$oid:
((activation / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"0321e89b" +
(accum++).toString().padStart(8, "0")
},
Node: node,
Activation: { $date: { $numberLong: activation.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
ActiveMissionTier: tier
});
}
};
const doesTimeSatsifyConstraints = (timeSecs: number): boolean => {
if (config.worldState?.eidolonOverride) {
const eidolonEpoch = 1391992660;
@ -1032,6 +1088,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
Sorties: [],
LiteSorties: [],
GlobalUpgrades: [],
VoidStorms: [],
EndlessXpChoices: [],
KnownCalendarSeasons: [],
...staticWorldState,
@ -1228,8 +1285,24 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
}
// Sentient Anomaly cycling every 30 minutes
// Void Storms
const hour = Math.trunc(timeMs / unixTimesInMs.hour);
const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute;
const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
if (overLastHourStormExpiry > timeMs) {
pushVoidStorms(worldState.VoidStorms, hour - 2);
}
pushVoidStorms(worldState.VoidStorms, hour - 1);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) {
pushVoidStorms(worldState.VoidStorms, hour);
}
// Sentient Anomaly + Xtra Cheese cycles
const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2));
const hourInSeconds = 3600;
const cheeseInterval = hourInSeconds * 8;
const cheeseDuration = hourInSeconds * 2;
const cheeseIndex = Math.trunc(timeSecs / cheeseInterval);
const tmp = {
cavabegin: "1690761600",
PurchasePlatformLockEnabled: true,
@ -1254,6 +1327,11 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
},
ennnd: true,
mbrt: true,
fbst: {
a: cheeseIndex * cheeseInterval, // This has a bug where the client shows a negative time for "Xtra cheese starts in ..." until it refreshes the world state. This is because we're only providing the new activation as soon as that time/date is reached. However, this is 100% faithful to live.
e: cheeseIndex * cheeseInterval + cheeseDuration,
n: (cheeseIndex + 1) * hourInSeconds * 8
},
sfn: [550, 553, 554, 555][halfHour % 4]
};
worldState.Tmp = JSON.stringify(tmp);

View File

@ -641,11 +641,11 @@ export interface IFocusUpgrade {
}
export interface IFocusXP {
AP_POWER: number;
AP_TACTIC: number;
AP_DEFENSE: number;
AP_ATTACK: number;
AP_WARD: number;
AP_POWER?: number;
AP_TACTIC?: number;
AP_DEFENSE?: number;
AP_ATTACK?: number;
AP_WARD?: number;
}
export type TFocusPolarity = keyof IFocusXP;

View File

@ -12,6 +12,7 @@ export interface IWorldState {
GlobalUpgrades: IGlobalUpgrade[];
ActiveMissions: IFissure[];
NodeOverrides: INodeOverride[];
VoidStorms: IVoidStorm[];
PVPChallengeInstances: IPVPChallengeInstance[];
EndlessXpChoices: IEndlessXpChoice[];
SeasonInfo?: {
@ -131,6 +132,14 @@ export interface ILiteSortie {
}[];
}
export interface IVoidStorm {
_id: IOid;
Node: string;
Activation: IMongoDate;
Expiry: IMongoDate;
ActiveMissionTier: string;
}
export interface IPVPChallengeInstance {
_id: IOid;
challengeTypeRefID: string;

View File

@ -1,61 +0,0 @@
{
"VendorInfo": {
"_id": {
"$oid": "5f456e00c96976e97d6b7fd7"
},
"TypeName": "/Lotus/Types/Game/VendorManifests/Deimos/ProspectorVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Gems/Deimos/DeimosUncommonGemACutItem",
"PremiumPrice": [13, 13],
"Bin": "BIN_1",
"QuantityMultiplier": 10,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e93a8"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Gems/Deimos/DeimosCommonGemBCutItem",
"PremiumPrice": [7, 7],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e93a9"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Gems/Deimos/DeimosCommonGemACutItem",
"PremiumPrice": [7, 7],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e93aa"
}
}
],
"PropertyTextHash": "2BBC116116C757F6AF4FBC3B9BF754C8",
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
}
}
}

View File

@ -1,125 +0,0 @@
{
"VendorInfo": {
"_id": {
"$oid": "5dbb4c41e966f7886c3ce939"
},
"TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/HarrowQuestKeyOrnament",
"ItemPrices": [
{
"ItemCount": 25,
"ItemType": "/Lotus/Types/Items/MiscItems/PrimeBucks",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "604800000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e945f"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack",
"ItemPrices": [
{
"ItemCount": 10,
"ItemType": "/Lotus/Types/Items/MiscItems/RivenFragment",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "604800000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"Id": {
"$oid": "66fd60b20ba592c4c95e9468"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/StoreItems/CreditBundles/150000Credits",
"ItemPrices": [
{
"ItemCount": 5,
"ItemType": "/Lotus/Types/Items/MiscItems/RivenFragment",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "604800000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"Id": {
"$oid": "66fd60b20ba592c4c95e9469"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Kuva",
"ItemPrices": [
{
"ItemCount": 10,
"ItemType": "/Lotus/Types/Items/MiscItems/RivenFragment",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 35000,
"Expiry": {
"$date": {
"$numberLong": "604800000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"Id": {
"$oid": "66fd60b20ba592c4c95e946a"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack",
"ItemPrices": [
{
"ItemCount": 10,
"ItemType": "/Lotus/Types/Items/MiscItems/RivenFragment",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "604800000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"Id": {
"$oid": "66fd60b20ba592c4c95e946b"
}
}
],
"PropertyTextHash": "62B64A8065B7C0FA345895D4BC234621",
"Expiry": {
"$date": {
"$numberLong": "604800000"
}
}
}
}

View File

@ -1,244 +0,0 @@
{
"VendorInfo": {
"_id": {
"$oid": "5fb70313c96976e97d6be787"
},
"TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/RailjackCrewMemberVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/SteelMeridianCrewMemberGeneratorStrong",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/RailjackMiscItems/IsosRailjackItem",
"ItemCount": 2220,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [2180000, 2180000],
"Bin": "BIN_2",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"Affiliation": "SteelMeridianSyndicate",
"MinAffiliationRank": 0,
"ReductionPerPositiveRank": 0.1,
"IncreasePerNegativeRank": 0.5,
"AllowMultipurchase": false,
"LocTagRandSeed": 4185144421,
"Id": {
"$oid": "670daf92d21f34757a5e73da"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/NewLokaCrewMemberGeneratorStrong",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/RailjackMiscItems/IsosRailjackItem",
"ItemCount": 2130,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [1890000, 1890000],
"Bin": "BIN_2",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"Affiliation": "NewLokaSyndicate",
"MinAffiliationRank": 0,
"ReductionPerPositiveRank": 0.1,
"IncreasePerNegativeRank": 0.5,
"AllowMultipurchase": false,
"LocTagRandSeed": 496053258,
"Id": {
"$oid": "670daf92d21f34757a5e73db"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/SteelMeridianCrewMemberGeneratorMediumVersionTwo",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/RailjackMiscItems/IsosRailjackItem",
"ItemCount": 440,
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"Affiliation": "SteelMeridianSyndicate",
"MinAffiliationRank": 0,
"ReductionPerPositiveRank": 0.1,
"IncreasePerNegativeRank": 0.5,
"AllowMultipurchase": false,
"LocTagRandSeed": 2078883475,
"Id": {
"$oid": "670daf92d21f34757a5e73dc"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/NewLokaCrewMemberGeneratorMediumVersionTwo",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/RailjackMiscItems/AsteriteRailjackItem",
"ItemCount": 730,
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"Affiliation": "NewLokaSyndicate",
"MinAffiliationRank": 0,
"ReductionPerPositiveRank": 0.1,
"IncreasePerNegativeRank": 0.5,
"AllowMultipurchase": false,
"LocTagRandSeed": 3890380934,
"Id": {
"$oid": "670daf92d21f34757a5e73dd"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/CephalonSudaCrewMemberGeneratorMediumVersionTwo",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/RailjackMiscItems/AsteriteRailjackItem",
"ItemCount": 720,
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"Affiliation": "CephalonSudaSyndicate",
"MinAffiliationRank": 0,
"ReductionPerPositiveRank": 0.1,
"IncreasePerNegativeRank": 0.5,
"AllowMultipurchase": false,
"LocTagRandSeed": 3425148044,
"Id": {
"$oid": "670daf92d21f34757a5e73de"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/ArbitersCrewMemberGeneratorMediumVersionTwo",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/RailjackMiscItems/CubicsRailjackItem",
"ItemCount": 6500,
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"Affiliation": "ArbitersSyndicate",
"MinAffiliationRank": 0,
"ReductionPerPositiveRank": 0.1,
"IncreasePerNegativeRank": 0.5,
"AllowMultipurchase": false,
"LocTagRandSeed": 2472754512,
"Id": {
"$oid": "670daf92d21f34757a5e73df"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/PerrinCrewMemberGeneratorVersionTwo",
"RegularPrice": [105000, 105000],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"Affiliation": "PerrinSyndicate",
"MinAffiliationRank": 0,
"ReductionPerPositiveRank": 0.1,
"IncreasePerNegativeRank": 0.5,
"AllowMultipurchase": false,
"LocTagRandSeed": 966238763,
"Id": {
"$oid": "670daf92d21f34757a5e73e0"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/NewLokaCrewMemberGeneratorVersionTwo",
"RegularPrice": [120000, 120000],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"Affiliation": "NewLokaSyndicate",
"MinAffiliationRank": 0,
"ReductionPerPositiveRank": 0.1,
"IncreasePerNegativeRank": 0.5,
"AllowMultipurchase": false,
"LocTagRandSeed": 356717213,
"Id": {
"$oid": "670daf92d21f34757a5e73e1"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Game/CrewShip/CrewMember/ArbitersCrewMemberGeneratorVersionTwo",
"RegularPrice": [120000, 120000],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"Affiliation": "ArbitersSyndicate",
"MinAffiliationRank": 0,
"ReductionPerPositiveRank": 0.1,
"IncreasePerNegativeRank": 0.5,
"AllowMultipurchase": false,
"LocTagRandSeed": 1969797050,
"Id": {
"$oid": "670daf92d21f34757a5e73e2"
}
}
],
"PropertyTextHash": "BE543CCC0A4F50A1D80CD2B523796EAE",
"RandomSeedType": "VRST_FLAVOUR_TEXT",
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
}
}
}

View File

@ -1,62 +0,0 @@
{
"VendorInfo": {
"_id": {
"$oid": "59dfe591314805ffe1d47c0a"
},
"TypeName": "/Lotus/Types/Game/VendorManifests/Ostron/ProspectorVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Gems/Eidolon/RareGemACutAItem",
"PremiumPrice": [19, 19],
"Bin": "BIN_1",
"QuantityMultiplier": 5,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e98f0"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Gems/Eidolon/CommonGemBCutAItem",
"PremiumPrice": [8, 8],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e98f1"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Gems/Eidolon/CommonGemACutAItem",
"PremiumPrice": [5, 5],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e98f2"
}
}
],
"MaxDailyPurchases": 0,
"PropertyTextHash": "773C6968D9A65506CD28DF28C768F0DA",
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
}
}
}

View File

@ -1,61 +0,0 @@
{
"VendorInfo": {
"_id": {
"$oid": "5be4a159b144f3cdf1c22ebb"
},
"TypeName": "/Lotus/Types/Game/VendorManifests/Solaris/ProspectorVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Gems/Solaris/SolarisRareGemACutItem",
"PremiumPrice": [20, 20],
"Bin": "BIN_1",
"QuantityMultiplier": 5,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e9777"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Gems/Solaris/SolarisCommonGemBCutItem",
"PremiumPrice": [10, 10],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e9778"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Gems/Solaris/SolarisCommonGemACutItem",
"PremiumPrice": [8, 8],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e9779"
}
}
],
"PropertyTextHash": "A5756A21991FF49CFA7D096B4026515B",
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
}
}
}

View File

@ -2562,50 +2562,6 @@
]
}
],
"VoidStorms": [
{
"_id": { "$oid": "663a7581ced28e18f694b550" },
"Node": "CrewBattleNode519",
"Activation": { "$date": { "$numberLong": "1715109601821" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"ActiveMissionTier": "VoidT1"
},
{
"_id": { "$oid": "663a7581ced28e18f694b551" },
"Node": "CrewBattleNode515",
"Activation": { "$date": { "$numberLong": "1715109601825" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"ActiveMissionTier": "VoidT1"
},
{
"_id": { "$oid": "663a7581ced28e18f694b554" },
"Node": "CrewBattleNode536",
"Activation": { "$date": { "$numberLong": "1715109601832" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"ActiveMissionTier": "VoidT4"
},
{
"_id": { "$oid": "663a7581ced28e18f694b555" },
"Node": "CrewBattleNode539",
"Activation": { "$date": { "$numberLong": "1715109601834" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"ActiveMissionTier": "VoidT4"
},
{
"_id": { "$oid": "663a7581ced28e18f694b553" },
"Node": "CrewBattleNode521",
"Activation": { "$date": { "$numberLong": "1715109601829" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"ActiveMissionTier": "VoidT3"
},
{
"_id": { "$oid": "663a7581ced28e18f694b552" },
"Node": "CrewBattleNode535",
"Activation": { "$date": { "$numberLong": "1715109601827" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"ActiveMissionTier": "VoidT2"
}
],
"PrimeAccessAvailability": { "State": "PRIME1" },
"PrimeVaultAvailabilities": [false, false, false, false, false],
"PrimeTokenAvailability": true,

View File

@ -29,7 +29,7 @@ function loginFromLocalStorage() {
},
() => {
logout();
alert(isRegister ? "Registration failed. Account already exists?" : "Login failed");
alert(loc(isRegister ? "code_regFail" : "code_loginFail"));
}
);
}

View File

@ -3,6 +3,8 @@ dict = {
general_inventoryUpdateNote: `Hinweis: Änderungen, die hier vorgenommen werden, werden erst im Spiel angewendet, sobald das Inventar synchronisiert wird. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`,
general_addButton: `Hinzufügen`,
general_bulkActions: `Massenaktionen`,
code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
code_nonValidAuthz: `Deine Anmeldedaten sind nicht mehr gültig.`,
code_changeNameConfirm: `In welchen Namen möchtest du deinen Account umbenennen?`,
code_deleteAccountConfirm: `Bist du sicher, dass du deinen Account |DISPLAYNAME| (|EMAIL|) löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.`,

View File

@ -2,6 +2,8 @@ dict = {
general_inventoryUpdateNote: `Note: Changes made here will only be applied in-game when the game syncs the inventory. Visiting the navigation should be the easiest way to trigger that.`,
general_addButton: `Add`,
general_bulkActions: `Bulk Actions`,
code_loginFail: `Login failed. Double-check the email and password.`,
code_regFail: `Registration failed. Account already exists?`,
code_nonValidAuthz: `Your credentials are no longer valid.`,
code_changeNameConfirm: `What would you like to change your account name to?`,
code_deleteAccountConfirm: `Are you sure you want to delete your account |DISPLAYNAME| (|EMAIL|)? This action cannot be undone.`,

View File

@ -3,6 +3,8 @@ dict = {
general_inventoryUpdateNote: `Nota: Los cambios realizados aquí se reflejarán en el juego cuando este sincronice el inventario. Usar la navegación debería ser la forma más sencilla de activar esto.`,
general_addButton: `Agregar`,
general_bulkActions: `Acciones masivas`,
code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
code_nonValidAuthz: `Tus credenciales no son válidas.`,
code_changeNameConfirm: `¿Qué nombre te gustaría ponerle a tu cuenta?`,
code_deleteAccountConfirm: `¿Estás seguro de que deseas eliminar tu cuenta |DISPLAYNAME| (|EMAIL|)? Esta acción es permanente.`,
@ -176,56 +178,56 @@ dict = {
import_importNote: `Puedes proporcionar una respuesta de inventario completa o parcial (representación del cliente) aquí. Todos los campos compatibles con el importador <b>serán sobrescritos</b> en tu cuenta.`,
import_submit: `Enviar`,
upgrade_Equilibrium: `[UNTRANSLATED] +|VAL|% Energy from Health pickups, +|VAL|% Health from Energy pickups`,
upgrade_MeleeCritDamage: `[UNTRANSLATED] +|VAL|% Melee Critical Damage`,
upgrade_PrimaryStatusChance: `[UNTRANSLATED] +|VAL|% Primary Status Chance`,
upgrade_SecondaryCritChance: `[UNTRANSLATED] +|VAL|% Secondary Critical Chance`,
upgrade_WarframeAbilityDuration: `[UNTRANSLATED] +|VAL|% Ability Duration`,
upgrade_WarframeAbilityStrength: `[UNTRANSLATED] +|VAL|% Ability Strength`,
upgrade_WarframeArmourMax: `[UNTRANSLATED] +|VAL| Armor`,
upgrade_WarframeBlastProc: `[UNTRANSLATED] +|VAL| Shields on inflicting Blast Status`,
upgrade_WarframeCastingSpeed: `[UNTRANSLATED] +|VAL|% Casting Speed`,
upgrade_WarframeCorrosiveDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Corrosion Status`,
upgrade_WarframeCorrosiveStack: `[UNTRANSLATED] Increase max stacks of Corrosion Status by +|VAL|`,
upgrade_WarframeCritDamageBoost: `[UNTRANSLATED] +|VAL|% Melee Critical Damage (Doubles over 500 Energy)`,
upgrade_WarframeElectricDamage: `[UNTRANSLATED] +|VAL1|% Primary Electricity Damage (+|VAL2|% per additional Shard)`,
upgrade_WarframeElectricDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Electricity Status`,
upgrade_WarframeEnergyMax: `[UNTRANSLATED] +|VAL| Energy Max`,
upgrade_WarframeGlobeEffectEnergy: `[UNTRANSLATED] +|VAL|% Energy Orb Effectiveness`,
upgrade_WarframeGlobeEffectHealth: `[UNTRANSLATED] +|VAL|% Health Orb Effectiveness`,
upgrade_WarframeHealthMax: `[UNTRANSLATED] +|VAL| Health`,
upgrade_WarframeHPBoostFromImpact: `[UNTRANSLATED] +|VAL1| Health per enemy killed with Blast Damage (Max |VAL2| Health)`,
upgrade_WarframeParkourVelocity: `[UNTRANSLATED] +|VAL|% Parkour Velocity`,
upgrade_WarframeRadiationDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Radiation Status`,
upgrade_WarframeRegen: `[UNTRANSLATED] +|VAL| Health Regen/s`,
upgrade_WarframeShieldMax: `[UNTRANSLATED] +|VAL| Shield`,
upgrade_WarframeStartingEnergy: `[UNTRANSLATED] +|VAL|% Energy on Spawn`,
upgrade_WarframeToxinDamage: `[UNTRANSLATED] +|VAL|% Toxin Status Effect Damage`,
upgrade_WarframeToxinHeal: `[UNTRANSLATED] +|VAL| Health on damaging enemies with Toxin Status`,
upgrade_WeaponCritBoostFromHeat: `[UNTRANSLATED] +|VAL1|% Secondary Critical Chance per Heat-affected enemy killed (Max |VAL2|%)`,
upgrade_AvatarAbilityRange: `[UNTRANSLATED] +7.5% Ability Range`,
upgrade_AvatarAbilityEfficiency: `[UNTRANSLATED] +5% Ability Efficiency`,
upgrade_AvatarEnergyRegen: `[UNTRANSLATED] +0.5 Energy Regen/s`,
upgrade_AvatarEnemyRadar: `[UNTRANSLATED] +5m Enemy Radar`,
upgrade_AvatarLootRadar: `[UNTRANSLATED] +7m Loot Radar`,
upgrade_WeaponAmmoMax: `[UNTRANSLATED] +15% Ammo Max`,
upgrade_EnemyArmorReductionAura: `[UNTRANSLATED] -3% Enemy Armor`,
upgrade_OnExecutionAmmo: `[UNTRANSLATED] 100% Primary and Secondary Magazine Refill on Mercy`,
upgrade_OnExecutionHealthDrop: `[UNTRANSLATED] 100% chance to drop a Health Orb on Mercy`,
upgrade_OnExecutionEnergyDrop: `[UNTRANSLATED] 50% chance to drop an Energy Orb on Mercy`,
upgrade_OnFailHackReset: `[UNTRANSLATED] +50% to retry on Hacking failure`,
upgrade_DamageReductionOnHack: `[UNTRANSLATED] 75% Damage Reduction while Hacking`,
upgrade_OnExecutionReviveCompanion: `[UNTRANSLATED] Mercy Kills reduce Companion Recovery by 15s`,
upgrade_OnExecutionParkourSpeed: `[UNTRANSLATED] +60% Parkour Speed after a Mercy for 15s`,
upgrade_AvatarTimeLimitIncrease: `[UNTRANSLATED] s to Hacking`,
upgrade_ElectrifyOnHack: `[UNTRANSLATED] Shock enemies within 20m while Hacking`,
upgrade_OnExecutionTerrify: `[UNTRANSLATED] 50% chance for enemies within 15m to cower in fear for 8 seconds on Mercy`,
upgrade_OnHackLockers: `[UNTRANSLATED] Unlock 5 lockers within 20m after Hacking`,
upgrade_OnExecutionBlind: `[UNTRANSLATED] Blind enemies within 18m on Mercy`,
upgrade_OnExecutionDrainPower: `[UNTRANSLATED] 100% chance for next ability cast to gain +50% Ability Strength on Mercy`,
upgrade_OnHackSprintSpeed: `[UNTRANSLATED] +75% Sprint Speed for 15s after Hacking`,
upgrade_SwiftExecute: `[UNTRANSLATED] Speed of Mercy Kills increased by 50%`,
upgrade_OnHackInvis: `[UNTRANSLATED] Invisible for 15 seconds after hacking`,
upgrade_Equilibrium: `+|VAL|% de Energía al recoger salud, +|VAL|% de Salud al recoger energía`,
upgrade_MeleeCritDamage: `+|VAL|% de daño crítico cuerpo a cuerpo`,
upgrade_PrimaryStatusChance: `+|VAL|% de probabilidad de estado en armas primarias`,
upgrade_SecondaryCritChance: `+|VAL|% de probabilidad crítica en armas secundarias`,
upgrade_WarframeAbilityDuration: `+|VAL|% de duración de habilidades`,
upgrade_WarframeAbilityStrength: `+|VAL|% de fuerza de habilidades`,
upgrade_WarframeArmourMax: `+|VAL| de armadura`,
upgrade_WarframeBlastProc: `+|VAL| de escudos al infligir estado de explosión`,
upgrade_WarframeCastingSpeed: `+|VAL|% de velocidad de lanzamiento de habilidades`,
upgrade_WarframeCorrosiveDamageBoost: `+|VAL|% de daño de habilidades a enemigos con estado corrosivo`,
upgrade_WarframeCorrosiveStack: `Aumenta los acumuladores máximos de estado corrosivo en +|VAL|`,
upgrade_WarframeCritDamageBoost: `+|VAL|% de daño crítico cuerpo a cuerpo (se duplica con más de 500 de energía)`,
upgrade_WarframeElectricDamage: `+|VAL1|% de daño eléctrico en armas primarias (+|VAL2|% por fragmento adicional)`,
upgrade_WarframeElectricDamageBoost: `+|VAL|% de daño de habilidades a enemigos con estado eléctrico`,
upgrade_WarframeEnergyMax: `+|VAL| de energía máxima`,
upgrade_WarframeGlobeEffectEnergy: `+|VAL|% de efectividad de orbes de energía`,
upgrade_WarframeGlobeEffectHealth: `+|VAL|% de efectividad de orbes de salud`,
upgrade_WarframeHealthMax: `+|VAL| de salud máxima`,
upgrade_WarframeHPBoostFromImpact: `+|VAL1| de salud por enemigo eliminado con daño explosivo (máximo |VAL2| de salud)`,
upgrade_WarframeParkourVelocity: `+|VAL|% de velocidad de parkour`,
upgrade_WarframeRadiationDamageBoost: `+|VAL|% de daño de habilidades a enemigos con estado radiactivo`,
upgrade_WarframeRegen: `+|VAL| de regeneración de salud por segundo`,
upgrade_WarframeShieldMax: `+|VAL| de escudo`,
upgrade_WarframeStartingEnergy: `+|VAL|% de energía al reaparecer`,
upgrade_WarframeToxinDamage: `+|VAL|% de daño por efecto de estado tóxico`,
upgrade_WarframeToxinHeal: `+|VAL| de salud al dañar enemigos con estado tóxico`,
upgrade_WeaponCritBoostFromHeat: `+|VAL1|% de probabilidad crítica secundaria por enemigo afectado por calor eliminado (máx. |VAL2|%)`,
upgrade_AvatarAbilityRange: `+7.5% de alcance de habilidades`,
upgrade_AvatarAbilityEfficiency: `+5% de eficiencia de habilidades`,
upgrade_AvatarEnergyRegen: `+0.5 de regeneración de energía por segundo`,
upgrade_AvatarEnemyRadar: `+5m de radar de enemigos`,
upgrade_AvatarLootRadar: `+7m de radar de botín`,
upgrade_WeaponAmmoMax: `+15% de munición máxima`,
upgrade_EnemyArmorReductionAura: `-3% de armadura enemiga`,
upgrade_OnExecutionAmmo: `Recarga al 100% el cargador primario y secundario tras ejecución (Misericordia)`,
upgrade_OnExecutionHealthDrop: `100% de probabilidad de soltar un orbe de salud tras ejecución (Misericordia)`,
upgrade_OnExecutionEnergyDrop: `50% de probabilidad de soltar un orbe de energía tras ejecución (Misericordia)`,
upgrade_OnFailHackReset: `+50% de probabilidad de reintento al fallar un hackeo`,
upgrade_DamageReductionOnHack: `75% de reducción de daño al hackear`,
upgrade_OnExecutionReviveCompanion: `Las ejecuciones reducen el tiempo de recuperación del compañero en 15s`,
upgrade_OnExecutionParkourSpeed: `+60% de velocidad de parkour durante 15s tras una ejecución`,
upgrade_AvatarTimeLimitIncrease: `+|VAL|s al tiempo de hackeo`,
upgrade_ElectrifyOnHack: `Electrocuta a los enemigos en un radio de 20m al hackear`,
upgrade_OnExecutionTerrify: `50% de probabilidad de que enemigos en un radio de 15m entren en pánico por 8s tras una ejecución`,
upgrade_OnHackLockers: `Desbloquea 5 casilleros en un radio de 20m tras hackear`,
upgrade_OnExecutionBlind: `Ciega a los enemigos en un radio de 18m tras una ejecución`,
upgrade_OnExecutionDrainPower: `100% de probabilidad de que la siguiente habilidad tenga +50% de fuerza tras una ejecución`,
upgrade_OnHackSprintSpeed: `+75% de velocidad de carrera durante 15s después de hackear`,
upgrade_SwiftExecute: `Velocidad de ejecuciones aumentada en un 50%`,
upgrade_OnHackInvis: `Invisible durante 15 segundos después de hackear`,
prettier_sucks_ass: ``
};

View File

@ -3,6 +3,8 @@ dict = {
general_inventoryUpdateNote: `Note : Les changements effectués ici seront appliqués lors de la syncrhonisation. Visiter la navigation appliquera les changements apportés à l'inventaire.`,
general_addButton: `Ajouter`,
general_bulkActions: `Action groupée`,
code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
code_nonValidAuthz: `Informations de connexion invalides`,
code_changeNameConfirm: `Nouveau nom du compte :`,
code_deleteAccountConfirm: `Supprimer |DISPLAYNAME| (|EMAIL|) ? Cette action est irreversible.`,

View File

@ -3,6 +3,8 @@ dict = {
general_inventoryUpdateNote: `Примечание: изменения, внесенные здесь, отобразятся в игре только после повторной загрузки вашего инвентаря. Посещение навигации — самый простой способ этого добиться.`,
general_addButton: `Добавить`,
general_bulkActions: `Массовые действия`,
code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
code_nonValidAuthz: `Ваши данные больше не действительны.`,
code_changeNameConfirm: `Какое имя вы хотите установить для своей учетной записи?`,
code_deleteAccountConfirm: `Вы уверены, что хотите удалить аккаунт |DISPLAYNAME| (|EMAIL|)? Это действие нельзя отменить.`,

View File

@ -3,6 +3,8 @@ dict = {
general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`,
general_addButton: `添加`,
general_bulkActions: `批量操作`,
code_loginFail: `登录失败。请检查邮箱和密码。`,
code_regFail: `注册失败。账号已存在。`,
code_nonValidAuthz: `您的登录凭证已失效。`,
code_changeNameConfirm: `您想将账户名称更改为什么?`,
code_deleteAccountConfirm: `确定要删除账户 |DISPLAYNAME| (|EMAIL|) 吗?此操作不可撤销。`,
@ -25,7 +27,7 @@ dict = {
code_renamePrompt: `输入新的自定义名称:`,
code_remove: `移除`,
code_addItemsConfirm: `确定要向账户添加 |COUNT| 件物品吗?`,
code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`,
code_succRankUp: `等级已提升`,
code_noEquipmentToRankUp: `没有可升级的装备。`,
code_succAdded: `已成功添加。`,
code_succRemoved: `已成功移除。`,
@ -34,8 +36,8 @@ dict = {
code_rerollsNumber: `洗卡次数`,
code_viewStats: `查看属性`,
code_rank: `等级`,
code_rankUp: `[UNTRANSLATED] Rank up`,
code_rankDown: `[UNTRANSLATED] Rank down`,
code_rankUp: `等级提升`,
code_rankDown: `等级下降`,
code_count: `数量`,
code_focusAllUnlocked: `所有专精学派均已解锁。`,
code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`,
@ -176,56 +178,56 @@ dict = {
import_importNote: `您可以在此处提供完整或部分库存响应(客户端表示)。支持的所有字段<b>将被覆盖</b>到您的账户中。`,
import_submit: `提交`,
upgrade_Equilibrium: `[UNTRANSLATED] +|VAL|% Energy from Health pickups, +|VAL|% Health from Energy pickups`,
upgrade_MeleeCritDamage: `[UNTRANSLATED] +|VAL|% Melee Critical Damage`,
upgrade_PrimaryStatusChance: `[UNTRANSLATED] +|VAL|% Primary Status Chance`,
upgrade_SecondaryCritChance: `[UNTRANSLATED] +|VAL|% Secondary Critical Chance`,
upgrade_WarframeAbilityDuration: `[UNTRANSLATED] +|VAL|% Ability Duration`,
upgrade_WarframeAbilityStrength: `[UNTRANSLATED] +|VAL|% Ability Strength`,
upgrade_WarframeArmourMax: `[UNTRANSLATED] +|VAL| Armor`,
upgrade_WarframeBlastProc: `[UNTRANSLATED] +|VAL| Shields on inflicting Blast Status`,
upgrade_WarframeCastingSpeed: `[UNTRANSLATED] +|VAL|% Casting Speed`,
upgrade_WarframeCorrosiveDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Corrosion Status`,
upgrade_WarframeCorrosiveStack: `[UNTRANSLATED] Increase max stacks of Corrosion Status by +|VAL|`,
upgrade_WarframeCritDamageBoost: `[UNTRANSLATED] +|VAL|% Melee Critical Damage (Doubles over 500 Energy)`,
upgrade_WarframeElectricDamage: `[UNTRANSLATED] +|VAL1|% Primary Electricity Damage (+|VAL2|% per additional Shard)`,
upgrade_WarframeElectricDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Electricity Status`,
upgrade_WarframeEnergyMax: `[UNTRANSLATED] +|VAL| Energy Max`,
upgrade_WarframeGlobeEffectEnergy: `[UNTRANSLATED] +|VAL|% Energy Orb Effectiveness`,
upgrade_WarframeGlobeEffectHealth: `[UNTRANSLATED] +|VAL|% Health Orb Effectiveness`,
upgrade_WarframeHealthMax: `[UNTRANSLATED] +|VAL| Health`,
upgrade_WarframeHPBoostFromImpact: `[UNTRANSLATED] +|VAL1| Health per enemy killed with Blast Damage (Max |VAL2| Health)`,
upgrade_WarframeParkourVelocity: `[UNTRANSLATED] +|VAL|% Parkour Velocity`,
upgrade_WarframeRadiationDamageBoost: `[UNTRANSLATED] +|VAL|% Ability Damage on enemies affected by Radiation Status`,
upgrade_WarframeRegen: `[UNTRANSLATED] +|VAL| Health Regen/s`,
upgrade_WarframeShieldMax: `[UNTRANSLATED] +|VAL| Shield`,
upgrade_WarframeStartingEnergy: `[UNTRANSLATED] +|VAL|% Energy on Spawn`,
upgrade_WarframeToxinDamage: `[UNTRANSLATED] +|VAL|% Toxin Status Effect Damage`,
upgrade_WarframeToxinHeal: `[UNTRANSLATED] +|VAL| Health on damaging enemies with Toxin Status`,
upgrade_WeaponCritBoostFromHeat: `[UNTRANSLATED] +|VAL1|% Secondary Critical Chance per Heat-affected enemy killed (Max |VAL2|%)`,
upgrade_AvatarAbilityRange: `[UNTRANSLATED] +7.5% Ability Range`,
upgrade_AvatarAbilityEfficiency: `[UNTRANSLATED] +5% Ability Efficiency`,
upgrade_AvatarEnergyRegen: `[UNTRANSLATED] +0.5 Energy Regen/s`,
upgrade_AvatarEnemyRadar: `[UNTRANSLATED] +5m Enemy Radar`,
upgrade_AvatarLootRadar: `[UNTRANSLATED] +7m Loot Radar`,
upgrade_WeaponAmmoMax: `[UNTRANSLATED] +15% Ammo Max`,
upgrade_EnemyArmorReductionAura: `[UNTRANSLATED] -3% Enemy Armor`,
upgrade_OnExecutionAmmo: `[UNTRANSLATED] 100% Primary and Secondary Magazine Refill on Mercy`,
upgrade_OnExecutionHealthDrop: `[UNTRANSLATED] 100% chance to drop a Health Orb on Mercy`,
upgrade_OnExecutionEnergyDrop: `[UNTRANSLATED] 50% chance to drop an Energy Orb on Mercy`,
upgrade_OnFailHackReset: `[UNTRANSLATED] +50% to retry on Hacking failure`,
upgrade_DamageReductionOnHack: `[UNTRANSLATED] 75% Damage Reduction while Hacking`,
upgrade_OnExecutionReviveCompanion: `[UNTRANSLATED] Mercy Kills reduce Companion Recovery by 15s`,
upgrade_OnExecutionParkourSpeed: `[UNTRANSLATED] +60% Parkour Speed after a Mercy for 15s`,
upgrade_AvatarTimeLimitIncrease: `[UNTRANSLATED] s to Hacking`,
upgrade_ElectrifyOnHack: `[UNTRANSLATED] Shock enemies within 20m while Hacking`,
upgrade_OnExecutionTerrify: `[UNTRANSLATED] 50% chance for enemies within 15m to cower in fear for 8 seconds on Mercy`,
upgrade_OnHackLockers: `[UNTRANSLATED] Unlock 5 lockers within 20m after Hacking`,
upgrade_OnExecutionBlind: `[UNTRANSLATED] Blind enemies within 18m on Mercy`,
upgrade_OnExecutionDrainPower: `[UNTRANSLATED] 100% chance for next ability cast to gain +50% Ability Strength on Mercy`,
upgrade_OnHackSprintSpeed: `[UNTRANSLATED] +75% Sprint Speed for 15s after Hacking`,
upgrade_SwiftExecute: `[UNTRANSLATED] Speed of Mercy Kills increased by 50%`,
upgrade_OnHackInvis: `[UNTRANSLATED] Invisible for 15 seconds after hacking`,
upgrade_Equilibrium: `+|VAL|% 能量 来自生命球, +|VAL|% 生命 来自能量球`,
upgrade_MeleeCritDamage: `+|VAL|% 近战暴击伤害`,
upgrade_PrimaryStatusChance: `+|VAL|% 主武器触发几率`,
upgrade_SecondaryCritChance: `+|VAL|% 次要武器暴击几率`,
upgrade_WarframeAbilityDuration: `+|VAL|% 技能持续时间`,
upgrade_WarframeAbilityStrength: `+|VAL|% 技能强度`,
upgrade_WarframeArmourMax: `+|VAL| 护甲`,
upgrade_WarframeBlastProc: `施加爆炸状态时,护盾 +|VAL|`,
upgrade_WarframeCastingSpeed: `+|VAL|% 施放速度`,
upgrade_WarframeCorrosiveDamageBoost: `对受腐蚀状态影响的敌人 +|VAL|% 技能伤害`,
upgrade_WarframeCorrosiveStack: `腐蚀状态最大堆叠数 +|VAL|`,
upgrade_WarframeCritDamageBoost: `+|VAL|% 近战暴击伤害 (500能量以上翻倍)`,
upgrade_WarframeElectricDamage: `+|VAL1|% 主武器伤害效果 (+|VAL2|% 每附加一个碎片)`,
upgrade_WarframeElectricDamageBoost: `对受电能状态影响的敌人 +|VAL|% 技能伤害`,
upgrade_WarframeEnergyMax: `+|VAL| 最大能量`,
upgrade_WarframeGlobeEffectEnergy: `+|VAL|% 能量球效果`,
upgrade_WarframeGlobeEffectHealth: `+|VAL|% 生命球效果`,
upgrade_WarframeHealthMax: `+|VAL| 生命`,
upgrade_WarframeHPBoostFromImpact: `每个被爆炸伤害击杀的敌人,补充 |VAL1|生命 (最大 |VAL2| 生命)`,
upgrade_WarframeParkourVelocity: `+|VAL|% 跑酷速度`,
upgrade_WarframeRadiationDamageBoost: `对受辐射状态影响的敌人 +|VAL|% 技能伤害`,
upgrade_WarframeRegen: `+|VAL| 生命再生/s`,
upgrade_WarframeShieldMax: `+|VAL| 护盾`,
upgrade_WarframeStartingEnergy: `+|VAL|% 能量出生时`,
upgrade_WarframeToxinDamage: `+|VAL|% 毒素伤害效果`,
upgrade_WarframeToxinHeal: `+|VAL| 生命 对毒素状态的敌人造成伤害时`,
upgrade_WeaponCritBoostFromHeat: `每个被火焰伤害杀死的敌人, 增加|VAL1|% 次要武器暴击几率 (最大 |VAL2|%)`,
upgrade_AvatarAbilityRange: `+7.5% 技能范围`,
upgrade_AvatarAbilityEfficiency: `+5% 技能效率`,
upgrade_AvatarEnergyRegen: `+0.5 能量再生/秒`,
upgrade_AvatarEnemyRadar: `+5米 敌方雷达`,
upgrade_AvatarLootRadar: `+7米 战利品雷达`,
upgrade_WeaponAmmoMax: `+15% 弹药最大容量`,
upgrade_EnemyArmorReductionAura: `-3% 敌方护甲`,
upgrade_OnExecutionAmmo: `怜悯之击 100% 补充主次要武器弹匣`,
upgrade_OnExecutionHealthDrop: `怜悯之击 100% 几率 掉落生命球`,
upgrade_OnExecutionEnergyDrop: `怜悯之击 50% 几率 掉落生命球`,
upgrade_OnFailHackReset: `+50% 在入侵失败时重试`,
upgrade_DamageReductionOnHack: `入侵时,+75% 伤害减免`,
upgrade_OnExecutionReviveCompanion: `怜悯之击 减少同伴复苏时间 15秒`,
upgrade_OnExecutionParkourSpeed: `怜悯之击 15秒内 +60% 跑酷速度`,
upgrade_AvatarTimeLimitIncrease: `增加入侵限制时间`,
upgrade_ElectrifyOnHack: `入侵时震慑20米之内的敌人`,
upgrade_OnExecutionTerrify: `怜悯之击 50% 几率让 15米 以内的敌人恐慌`,
upgrade_OnHackLockers: `入侵后解锁20米内的5个储物柜`,
upgrade_OnExecutionBlind: `怜悯之击 致盲18米之内的敌人`,
upgrade_OnExecutionDrainPower: `怜悯之击会使下一个技能有100%的机会获得+50%的技能强度`,
upgrade_OnHackSprintSpeed: `入侵后+75%冲刺速度持续15秒`,
upgrade_SwiftExecute: `怜悯之击速度提升50%`,
upgrade_OnHackInvis: `入侵后隐身15秒`,
prettier_sucks_ass: ``
};