Compare commits

...

10 Commits

Author SHA1 Message Date
77a4ccde9d feat(webui): Change weapon Modular Parts 2025-07-11 16:09:52 +02:00
f796f9a851 feat: resetQuestProgress (#2461)
Just giving the client an 'ok' response. It seems that it does use updateQuest to manage the state itself mostly, just the server and webui are a bit confused about a quest with all stages completed still being active.
Re #1323

Reviewed-on: OpenWF/SpaceNinjaServer#2461
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-10 20:59:39 -07:00
e18b8e09ea fix: properly track xp for modular items (#2460)
Closes #2454

Reviewed-on: OpenWF/SpaceNinjaServer#2460
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-10 20:59:32 -07:00
0d8044b87c chore(webui): update to Spanish translation (#2466)
Reviewed-on: OpenWF/SpaceNinjaServer#2466
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-07-10 20:59:22 -07:00
a109ea6c5d chore: update PE+ (#2459)
Closes #2455

Reviewed-on: OpenWF/SpaceNinjaServer#2459
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-09 21:22:23 -07:00
7eb95c995c feat: initial invasions (#2458)
A rough generation of 3 invasions that change at daily reset, so missing the planet-based invasion 'chains'.
Battle pay is fully working tho, just a few points of uncertainty there due to missing research and logs.
Death marks are also roughly working.
Re #1097

Reviewed-on: OpenWF/SpaceNinjaServer#2458
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-09 19:58:01 -07:00
dc8f32d4d8 chore(webui): update Chinese translation (#2453)
Reviewed-on: OpenWF/SpaceNinjaServer#2453
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-08 22:12:26 -07:00
ba70ba88dd fix(webui): recreate missing datalist-QuestKeys entries after refreshing inventory (#2452)
Closes #2448

Reviewed-on: OpenWF/SpaceNinjaServer#2452
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:45 -07:00
08d4a03c50 fix: use the correct magic number for crew member seeds (#2451)
Closes #2444

Reviewed-on: OpenWF/SpaceNinjaServer#2451
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:24 -07:00
45feff682b feat: give on call crew gear item for command rank 9 (#2450)
Closes #2445

Reviewed-on: OpenWF/SpaceNinjaServer#2450
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:14 -07:00
23 changed files with 759 additions and 128 deletions

8
package-lock.json generated
View File

@ -23,7 +23,7 @@
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"undici": "^7.10.0", "undici": "^7.10.0",
"warframe-public-export-plus": "^0.5.77", "warframe-public-export-plus": "^0.5.78",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^5.0.0",
@ -5479,9 +5479,9 @@
} }
}, },
"node_modules/warframe-public-export-plus": { "node_modules/warframe-public-export-plus": {
"version": "0.5.77", "version": "0.5.78",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.77.tgz", "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.78.tgz",
"integrity": "sha512-Th/b82pYB4i95afC/s8MDDXsVMl3vyy1qDwLO/AzV6peVBTs2kCSO68nNh7yXDiviGlNl0HRq+LR25jMjtFwSg==" "integrity": "sha512-Zvg7N+EdXS8cOAZIxqCbqiqyvQZBgh2xTxEwpHnoyJjNBpm3sP/7dtXmzHaxAZjyaCL4pvi9e7kTvxmpH8Pcag=="
}, },
"node_modules/warframe-riven-info": { "node_modules/warframe-riven-info": {
"version": "0.1.2", "version": "0.1.2",

View File

@ -39,7 +39,7 @@
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"undici": "^7.10.0", "undici": "^7.10.0",
"warframe-public-export-plus": "^0.5.77", "warframe-public-export-plus": "^0.5.78",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^5.0.0",

View File

@ -6,10 +6,18 @@ import allDialogue from "@/static/fixed_responses/allDialogue.json";
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPolarity, ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IPolarity, ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportCustoms, ExportFlavour, ExportResources, ExportVirtuals } from "warframe-public-export-plus"; import {
eFaction,
ExportCustoms,
ExportFlavour,
ExportResources,
ExportVirtuals,
ICountedItem
} from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import { import {
addEmailItem, addEmailItem,
addItem,
addMiscItems, addMiscItems,
allDailyAffiliationKeys, allDailyAffiliationKeys,
checkCalendarAutoAdvance, checkCalendarAutoAdvance,
@ -30,7 +38,8 @@ import { unixTimesInMs } from "@/src/constants/timeConstants";
import { DailyDeal } from "@/src/models/worldStateModel"; import { DailyDeal } from "@/src/models/worldStateModel";
import { EquipmentFeatures } from "@/src/types/equipmentTypes"; import { EquipmentFeatures } from "@/src/types/equipmentTypes";
import { generateRewardSeed } from "@/src/services/rngService"; import { generateRewardSeed } from "@/src/services/rngService";
import { getWorldState } from "@/src/services/worldStateService"; import { getInvasionByOid, getWorldState } from "@/src/services/worldStateService";
import { createMessage } from "@/src/services/inboxService";
export const inventoryController: RequestHandler = async (request, response) => { export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request); const account = await getAccountForRequest(request);
@ -186,6 +195,63 @@ export const inventoryController: RequestHandler = async (request, response) =>
//await inventory.save(); //await inventory.save();
} }
for (let i = 0; i != inventory.QualifyingInvasions.length; ) {
const qi = inventory.QualifyingInvasions[i];
const invasion = getInvasionByOid(qi.invasionId.toString());
if (!invasion) {
logger.debug(`removing QualifyingInvasions entry for unknown invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
if (invasion.Completed) {
let factionSidedWith: string | undefined;
let battlePay: ICountedItem[] | undefined;
if (qi.AttackerScore >= 3) {
factionSidedWith = invasion.Faction;
battlePay = invasion.AttackerReward.countedItems;
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
} else if (qi.DefenderScore >= 3) {
factionSidedWith = invasion.DefenderFaction;
battlePay = invasion.DefenderReward.countedItems;
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
}
if (factionSidedWith) {
if (battlePay) {
// Decoupling rewards from the inbox message because it may delete itself without being read
for (const item of battlePay) {
await addItem(inventory, item.ItemType, item.ItemCount);
}
await createMessage(account._id, [
{
sndr: eFaction.find(x => x.tag == factionSidedWith)?.name ?? factionSidedWith, // TOVERIFY
msg: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageBody`,
sub: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageSubject`,
countedAtt: battlePay,
attVisualOnly: true,
icon:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Interface/Icons/Npcs/EliteRifleLancerAvatar.png" // Source: https://www.reddit.com/r/Warframe/comments/1aj4usx/battle_pay_worth_10_plat/, https://www.youtube.com/watch?v=XhNZ6ai6BOY
: "/Lotus/Interface/Icons/Npcs/CrewmanNormal.png", // My best source for this is https://www.youtube.com/watch?v=rxrCCFm73XE around 1:37
// TOVERIFY: highPriority?
endDate: new Date(Date.now() + 86400_000) // TOVERIFY: This type of inbox message seems to automatically delete itself. We'll just delete it after 24 hours, but it's not clear if this is correct.
}
]);
}
if (invasion.Faction != "FC_INFESTATION") {
// Sided with grineer -> opposed corpus -> send zanuka (harvester)
// Sided with corpus -> opposed grineer -> send g3 (death squad)
inventory[factionSidedWith != "FC_GRINEER" ? "DeathSquadable" : "Harvestable"] = true;
// TOVERIFY: Should this happen earlier?
// TOVERIFY: Should this send an (ephemeral) email?
}
}
logger.debug(`removing QualifyingInvasions entry for completed invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
++i;
}
if (inventory.LastInventorySync) { if (inventory.LastInventorySync) {
const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000); const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000);
const currentDuviriMood = Math.trunc(Date.now() / 7200000); const currentDuviriMood = Math.trunc(Date.now() / 7200000);

View File

@ -1,25 +1,39 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService"; import { addConsumables, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const playerSkillsController: RequestHandler = async (req, res) => { export const playerSkillsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "PlayerSkills"); const inventory = await getInventory(accountId, "PlayerSkills Consumables");
const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body)); const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body));
const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]; const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills];
const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000; const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000;
inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost; inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost;
inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++; inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++;
await inventory.save();
const inventoryChanges: IInventoryChanges = {};
if (request.Skill == "LPS_COMMAND" && inventory.PlayerSkills.LPS_COMMAND == 9) {
const consumablesChanges = [
{
ItemType: "/Lotus/Types/Restoratives/Consumable/CrewmateBall",
ItemCount: 1
}
];
addConsumables(inventory, consumablesChanges);
inventoryChanges.Consumables = consumablesChanges;
}
await inventory.save();
res.json({ res.json({
Pool: request.Pool, Pool: request.Pool,
PoolInc: -cost, PoolInc: -cost,
Skill: request.Skill, Skill: request.Skill,
Rank: oldRank + 1 Rank: oldRank + 1,
InventoryChanges: inventoryChanges
}); });
}; };

View File

@ -0,0 +1,5 @@
import { RequestHandler } from "express";
export const resetQuestProgressController: RequestHandler = (_req, res) => {
res.send("1").end();
};

View File

@ -0,0 +1,65 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const changeModularPartsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as IUpdateFingerPrintRequest;
const inventory = await getInventory(accountId, request.category);
const item = inventory[request.category].id(request.oid);
if (item) {
item.ModularParts = request.modularParts;
request.modularParts.forEach(part => {
const categoryMap = mapping[part];
if (categoryMap && categoryMap[request.category]) {
item.ItemType = categoryMap[request.category]!;
}
});
await inventory.save();
}
res.end();
};
interface IUpdateFingerPrintRequest {
category: TEquipmentKey;
oid: string;
modularParts: string[];
}
const mapping: Partial<Record<string, Partial<Record<TEquipmentKey, string>>>> = {
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelAPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun"
},
"/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelEgg/InfModularBarrelEggPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun"
},
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelBPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary"
},
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelCPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary"
},
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelDPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
},
"/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelBeam/InfModularBarrelBeamPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
},
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": {
MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit"
},
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": {
MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit"
},
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": {
MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit"
}
};

View File

@ -112,6 +112,7 @@ import { removeFromGuildController } from "@/src/controllers/api/removeFromGuild
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
import { renamePetController } from "@/src/controllers/api/renamePetController"; import { renamePetController } from "@/src/controllers/api/renamePetController";
import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
import { resetQuestProgressController } from "@/src/controllers/api/resetQuestProgressController";
import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController";
import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
import { saveLoadoutController } from "@/src/controllers/api/saveLoadoutController"; import { saveLoadoutController } from "@/src/controllers/api/saveLoadoutController";
@ -209,6 +210,7 @@ apiRouter.get("/questControl.php", questControlController);
apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController); apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController);
apiRouter.get("/removeFriend.php", removeFriendGetController); apiRouter.get("/removeFriend.php", removeFriendGetController);
apiRouter.get("/removeFromAlliance.php", removeFromAllianceController); apiRouter.get("/removeFromAlliance.php", removeFromAllianceController);
apiRouter.get("/resetQuestProgress.php", resetQuestProgressController);
apiRouter.get("/setActiveQuest.php", setActiveQuestController); apiRouter.get("/setActiveQuest.php", setActiveQuestController);
apiRouter.get("/setActiveShip.php", setActiveShipController); apiRouter.get("/setActiveShip.php", setActiveShipController);
apiRouter.get("/setAllianceGuildPermissions.php", setAllianceGuildPermissionsController); apiRouter.get("/setAllianceGuildPermissions.php", setAllianceGuildPermissionsController);

View File

@ -25,6 +25,7 @@ import { manageQuestsController } from "@/src/controllers/custom/manageQuestsCon
import { setEvolutionProgressController } from "@/src/controllers/custom/setEvolutionProgressController"; import { setEvolutionProgressController } from "@/src/controllers/custom/setEvolutionProgressController";
import { setBoosterController } from "@/src/controllers/custom/setBoosterController"; import { setBoosterController } from "@/src/controllers/custom/setBoosterController";
import { updateFingerprintController } from "@/src/controllers/custom/updateFingerprintController"; import { updateFingerprintController } from "@/src/controllers/custom/updateFingerprintController";
import { changeModularPartsController } from "@/src/controllers/custom/changeModularPartsController";
import { getConfigController, setConfigController } from "@/src/controllers/custom/configController"; import { getConfigController, setConfigController } from "@/src/controllers/custom/configController";
@ -55,6 +56,7 @@ customRouter.post("/manageQuests", manageQuestsController);
customRouter.post("/setEvolutionProgress", setEvolutionProgressController); customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
customRouter.post("/setBooster", setBoosterController); customRouter.post("/setBooster", setBoosterController);
customRouter.post("/updateFingerprint", updateFingerprintController); customRouter.post("/updateFingerprint", updateFingerprintController);
customRouter.post("/changeModularParts", changeModularPartsController);
customRouter.post("/getConfig", getConfigController); customRouter.post("/getConfig", getConfigController);
customRouter.post("/setConfig", setConfigController); customRouter.post("/setConfig", setConfigController);

View File

@ -815,7 +815,7 @@ export const addItem = async (
if (!seed) { if (!seed) {
throw new Error(`Expected crew member to have a seed`); throw new Error(`Expected crew member to have a seed`);
} }
seed |= 0x33b81en << 32n; seed |= BigInt(Math.trunc(inventory.Created.getTime() / 1000) & 0xffffff) << 32n;
return { return {
...addCrewMember(inventory, typeName, seed), ...addCrewMember(inventory, typeName, seed),
...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase) ...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase)
@ -1635,6 +1635,15 @@ export const addEmailItem = async (
return inventoryChanges; return inventoryChanges;
}; };
const xpEarningParts: readonly string[] = [
"LWPT_BLADE",
"LWPT_GUN_BARREL",
"LWPT_AMP_OCULUS",
"LWPT_MOA_HEAD",
"LWPT_ZANUKA_HEAD",
"LWPT_HB_DECK"
];
export const applyClientEquipmentUpdates = ( export const applyClientEquipmentUpdates = (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
gearArray: IEquipmentClient[], gearArray: IEquipmentClient[],
@ -1653,13 +1662,26 @@ export const applyClientEquipmentUpdates = (
item.XP ??= 0; item.XP ??= 0;
item.XP += XP; item.XP += XP;
const xpinfoIndex = inventory.XPInfo.findIndex(x => x.ItemType == item.ItemType); let xpItemType = item.ItemType;
if (item.ModularParts) {
for (const part of item.ModularParts) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const partType = ExportWeapons[part]?.partType;
if (partType !== undefined && xpEarningParts.indexOf(partType) != -1) {
xpItemType = part;
break;
}
}
logger.debug(`adding xp to ${xpItemType} for modular item ${fromOid(ItemId)} (${item.ItemType})`);
}
const xpinfoIndex = inventory.XPInfo.findIndex(x => x.ItemType == xpItemType);
if (xpinfoIndex !== -1) { if (xpinfoIndex !== -1) {
const xpinfo = inventory.XPInfo[xpinfoIndex]; const xpinfo = inventory.XPInfo[xpinfoIndex];
xpinfo.XP += XP; xpinfo.XP += XP;
} else { } else {
inventory.XPInfo.push({ inventory.XPInfo.push({
ItemType: item.ItemType, ItemType: xpItemType,
XP: XP XP: XP
}); });
} }

View File

@ -558,6 +558,7 @@ export const addMissionInventoryUpdates = async (
} }
]); ]);
} }
inventory.DeathSquadable = false;
break; break;
} }
case "LockedWeaponGroup": { case "LockedWeaponGroup": {
@ -576,7 +577,7 @@ export const addMissionInventoryUpdates = async (
break; break;
} }
case "IncHarvester": { case "IncHarvester": {
inventory.Harvestable = true; // Unsure what to do with this
break; break;
} }
case "CurrentLoadOutIds": { case "CurrentLoadOutIds": {

View File

@ -6,15 +6,18 @@ import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.j
import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json"; import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json";
import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json"; import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json";
import darvoDeals from "@/static/fixed_responses/worldState/darvoDeals.json"; import darvoDeals from "@/static/fixed_responses/worldState/darvoDeals.json";
import invasionNodes from "@/static/fixed_responses/worldState/invasionNodes.json";
import invasionRewards from "@/static/fixed_responses/worldState/invasionRewards.json";
import { buildConfig } from "@/src/services/buildConfigService"; import { buildConfig } from "@/src/services/buildConfigService";
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "@/src/services/rngService"; import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "@/src/services/rngService";
import { eMissionType, ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus"; import { eMissionType, ExportRegions, ExportSyndicates, IMissionReward, IRegion } from "warframe-public-export-plus";
import { import {
ICalendarDay, ICalendarDay,
ICalendarEvent, ICalendarEvent,
ICalendarSeason, ICalendarSeason,
IInvasion,
ILiteSortie, ILiteSortie,
IPrimeVaultTrader, IPrimeVaultTrader,
IPrimeVaultTraderOffer, IPrimeVaultTraderOffer,
@ -1227,6 +1230,78 @@ const getAllVarziaManifests = (): IPrimeVaultTraderOffer[] => {
return [...dualPacks, ...singlePacks, ...items, ...bobbleHeads, ...relics]; return [...dualPacks, ...singlePacks, ...items, ...bobbleHeads, ...relics];
}; };
const createInvasion = (day: number, idx: number): IInvasion => {
const id = day * 3 + idx;
const defender = (["FC_GRINEER", "FC_CORPUS", day % 2 ? "FC_GRINEER" : "FC_CORPUS"] as const)[idx];
const rng = new SRng(new SRng(id).randomInt(0, 1_000_000));
const isInfestationOutbreak = rng.randomInt(0, 1) == 0;
const attacker = isInfestationOutbreak ? "FC_INFESTATION" : defender == "FC_GRINEER" ? "FC_CORPUS" : "FC_GRINEER";
const startMs = EPOCH + day * 86400_000;
const oid =
((startMs / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"fd148cb8" +
(idx & 0xffffffff).toString(16).padStart(8, "0");
const node = sequentiallyUniqueRandomElement(invasionNodes[defender], id, 5, 690175)!; // Can't repeat the other 2 on this day nor the last 3
const progress = (Date.now() - startMs) / 86400_000;
const countMultiplier = isInfestationOutbreak || rng.randomInt(0, 1) ? -1 : 1; // if defender is winning, count is negative
const fiftyPercent = rng.randomInt(1000, 29000); // introduce some 'yitter' for the percentages
const rewardFloat = rng.randomFloat();
const rewardTier = rewardFloat < 0.201 ? "RARE" : rewardFloat < 0.7788 ? "COMMON" : "UNCOMMON";
const attackerReward: IMissionReward = {};
const defenderReward: IMissionReward = {};
if (isInfestationOutbreak) {
defenderReward.countedItems = [
rng.randomElement(invasionRewards[rng.randomInt(0, 1) ? "FC_INFESTATION" : defender][rewardTier])!
];
} else {
attackerReward.countedItems = [rng.randomElement(invasionRewards[attacker][rewardTier])!];
defenderReward.countedItems = [rng.randomElement(invasionRewards[defender][rewardTier])!];
}
return {
_id: { $oid: oid },
Faction: attacker,
DefenderFaction: defender,
Node: node,
Count: Math.round(
(progress < 0.5 ? progress * 2 * fiftyPercent : fiftyPercent + (30_000 - fiftyPercent) * (progress - 0.5)) *
countMultiplier
),
Goal: 30000, // Value seems to range from 30000 to 98000 in intervals of 1000. Higher values are increasingly rare. I don't think this is relevant for the frontend besides dividing count by it.
LocTag: isInfestationOutbreak
? ExportRegions[node].missionIndex == 0
? "/Lotus/Language/Menu/InfestedInvasionBoss"
: "/Lotus/Language/Menu/InfestedInvasionGeneric"
: attacker == "FC_CORPUS"
? "/Lotus/Language/Menu/CorpusInvasionGeneric"
: "/Lotus/Language/Menu/GrineerInvasionGeneric",
Completed: startMs + 86400_000 < Date.now(), // Sorta unfaithful. Invasions on live are (at least in part) in fluenced by people completing them. And otherwise also probably not hardcoded to last 24 hours.
ChainID: { $oid: oid },
AttackerReward: attackerReward,
AttackerMissionInfo: {
seed: rng.randomInt(0, 1_000_000),
faction: defender
},
DefenderReward: defenderReward,
DefenderMissionInfo: {
seed: rng.randomInt(0, 1_000_000),
faction: attacker
},
Activation: {
$date: {
$numberLong: startMs.toString()
}
}
};
};
export const getInvasionByOid = (oid: string): IInvasion | undefined => {
const arr = oid.split("fd148cb8");
if (arr.length == 2 && arr[0].length == 8 && arr[1].length == 8) {
return createInvasion(idToDay(oid), parseInt(arr[1], 16));
}
return undefined;
};
export const getWorldState = (buildLabel?: string): IWorldState => { export const getWorldState = (buildLabel?: string): IWorldState => {
const constraints: ITimeConstraint[] = []; const constraints: ITimeConstraint[] = [];
if (config.worldState?.eidolonOverride) { if (config.worldState?.eidolonOverride) {
@ -1275,6 +1350,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
LiteSorties: [], LiteSorties: [],
ActiveMissions: [], ActiveMissions: [],
GlobalUpgrades: [], GlobalUpgrades: [],
Invasions: [],
VoidTraders: [], VoidTraders: [],
PrimeVaultTraders: [], PrimeVaultTraders: [],
VoidStorms: [], VoidStorms: [],
@ -1477,6 +1553,20 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
}); });
} }
// Rough outline of dynamic invasions.
// TODO: Invasions chains, e.g. an infestation mission would soon lead to other nodes on that planet also having an infestation invasion.
// TODO: Grineer/Corpus to fund their death stars with each invasion win.
{
worldState.Invasions.push(createInvasion(day, 0));
worldState.Invasions.push(createInvasion(day, 1));
worldState.Invasions.push(createInvasion(day, 2));
// Completed invasions stay for up to 24 hours as the winner 'occupies' that node
worldState.Invasions.push(createInvasion(day - 1, 0));
worldState.Invasions.push(createInvasion(day - 1, 1));
worldState.Invasions.push(createInvasion(day - 1, 2));
}
// Baro // Baro
{ {
const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14)); const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14));

View File

@ -12,6 +12,7 @@ export interface IWorldState {
SyndicateMissions: ISyndicateMissionInfo[]; SyndicateMissions: ISyndicateMissionInfo[];
ActiveMissions: IFissure[]; ActiveMissions: IFissure[];
GlobalUpgrades: IGlobalUpgrade[]; GlobalUpgrades: IGlobalUpgrade[];
Invasions: IInvasion[];
NodeOverrides: INodeOverride[]; NodeOverrides: INodeOverride[];
VoidTraders: IVoidTrader[]; VoidTraders: IVoidTrader[];
PrimeVaultTraders: IPrimeVaultTrader[]; PrimeVaultTraders: IPrimeVaultTrader[];
@ -82,6 +83,28 @@ export interface IGlobalUpgrade {
LocalizeDescTag: string; LocalizeDescTag: string;
} }
export interface IInvasion {
_id: IOid;
Faction: string;
DefenderFaction: string;
Node: string;
Count: number;
Goal: number;
LocTag: string;
Completed: boolean;
ChainID: IOid;
AttackerReward: IMissionReward;
AttackerMissionInfo: IInvasionMissionInfo;
DefenderReward: IMissionReward;
DefenderMissionInfo: IInvasionMissionInfo;
Activation: IMongoDate;
}
export interface IInvasionMissionInfo {
seed: number;
faction: string;
}
export interface IFissure { export interface IFissure {
_id: IOid; _id: IOid;
Region: number; Region: number;

View File

@ -0,0 +1,114 @@
{
"FC_CORPUS": [
"SettlementNode1",
"SettlementNode2",
"SettlementNode3",
"SettlementNode11",
"SettlementNode12",
"SettlementNode14",
"SettlementNode15",
"SettlementNode20",
"SolNode1",
"SolNode2",
"SolNode4",
"SolNode6",
"SolNode10",
"SolNode17",
"SolNode21",
"SolNode22",
"SolNode23",
"SolNode25",
"SolNode38",
"SolNode43",
"SolNode48",
"SolNode49",
"SolNode51",
"SolNode53",
"SolNode56",
"SolNode57",
"SolNode61",
"SolNode62",
"SolNode65",
"SolNode66",
"SolNode72",
"SolNode73",
"SolNode74",
"SolNode76",
"SolNode78",
"SolNode81",
"SolNode84",
"SolNode88",
"SolNode97",
"SolNode100",
"SolNode101",
"SolNode102",
"SolNode104",
"SolNode107",
"SolNode109",
"SolNode118",
"SolNode121",
"SolNode123",
"SolNode125",
"SolNode126",
"SolNode127",
"SolNode128",
"SolNode203",
"SolNode205",
"SolNode209",
"SolNode210",
"SolNode211",
"SolNode212",
"SolNode214",
"SolNode216",
"SolNode217",
"SolNode220"
],
"FC_GRINEER": [
"SolNode11",
"SolNode16",
"SolNode18",
"SolNode19",
"SolNode20",
"SolNode30",
"SolNode31",
"SolNode32",
"SolNode36",
"SolNode41",
"SolNode42",
"SolNode45",
"SolNode46",
"SolNode50",
"SolNode58",
"SolNode67",
"SolNode68",
"SolNode70",
"SolNode82",
"SolNode93",
"SolNode96",
"SolNode99",
"SolNode106",
"SolNode113",
"SolNode131",
"SolNode132",
"SolNode135",
"SolNode137",
"SolNode138",
"SolNode139",
"SolNode140",
"SolNode141",
"SolNode144",
"SolNode146",
"SolNode147",
"SolNode149",
"SolNode177",
"SolNode181",
"SolNode184",
"SolNode185",
"SolNode187",
"SolNode188",
"SolNode189",
"SolNode191",
"SolNode195",
"SolNode196"
]
}

View File

@ -0,0 +1,190 @@
{
"FC_GRINEER": {
"COMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/ChemComponent",
"ItemCount": 3
}
],
"UNCOMMON": [
{
"ItemType": "/Lotus/Types/Recipes/Weapons/KarakWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/StrunWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/LatronWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/TwinVipersWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithLink",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/GrineerCombatKnifeSortieBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeHilt",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeBlade",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeHeatsink",
"ItemCount": 1
}
],
"RARE": [
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinReactorBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/FormaBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/UtilityUnlockerBlueprint",
"ItemCount": 1
}
]
},
"FC_CORPUS": {
"COMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/EnergyComponent",
"ItemCount": 3
}
],
"UNCOMMON": [
{
"ItemType": "/Lotus/Types/Recipes/Weapons/DeraVandalBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/SnipetronVandalBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalBarrel",
"ItemCount": 1
}
],
"RARE": [
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinReactorBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/FormaBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/UtilityUnlockerBlueprint",
"ItemCount": 1
}
]
},
"FC_INFESTATION": {
"COMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/BioComponent",
"ItemCount": 1
}
],
"UNCOMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/BioComponent",
"ItemCount": 2
}
],
"RARE": [
{
"ItemType": "/Lotus/Types/Items/MiscItems/InfestedAladCoordinate",
"ItemCount": 1
}
]
}
}

View File

@ -117,46 +117,6 @@
] ]
} }
}, },
"Invasions": [
{
"_id": {
"$oid": "67c8ec8b3d0d86b236c1c18f"
},
"Faction": "FC_INFESTATION",
"DefenderFaction": "FC_CORPUS",
"Node": "SolNode53",
"Count": -28558,
"Goal": 30000,
"LocTag": "/Lotus/Language/Menu/InfestedInvasionBoss",
"Completed": false,
"ChainID": {
"$oid": "67c8b6a2bde0dfd0f7c1c18d"
},
"AttackerReward": [],
"AttackerMissionInfo": {
"seed": 488863,
"faction": "FC_CORPUS"
},
"DefenderReward": {
"countedItems": [
{
"ItemType": "/Lotus/Types/Items/Research/EnergyComponent",
"ItemCount": 3
}
]
},
"DefenderMissionInfo": {
"seed": 127653,
"faction": "FC_INFESTATION",
"missionReward": []
},
"Activation": {
"$date": {
"$numberLong": "1741221003031"
}
}
}
],
"SyndicateMissions": [ "SyndicateMissions": [
{ {
"_id": { "$oid": "663a4fc5ba6f84724fa4804c" }, "_id": { "$oid": "663a4fc5ba6f84724fa4804c" },

View File

@ -478,6 +478,12 @@
</table> </table>
</div> </div>
</div> </div>
<div id="modularParts-card" class="card mb-3 d-none">
<h5 class="card-header" data-loc="detailedView_modularPartsLabel"></h5>
<div class="card-body">
<form id="modularParts-form" class="input-group mb-3" onsubmit="handleModularPartsChange(event)"></form>
</div>
</div>
<div id="valenceBonus-card" class="card mb-3 d-none"> <div id="valenceBonus-card" class="card mb-3 d-none">
<h5 class="card-header" data-loc="detailedView_valenceBonusLabel"></h5> <h5 class="card-header" data-loc="detailedView_valenceBonusLabel"></h5>
<div class="card-body"> <div class="card-body">

View File

@ -273,6 +273,8 @@ function fetchItemList() {
window.itemListPromise = new Promise(resolve => { window.itemListPromise = new Promise(resolve => {
const req = $.get("/custom/getItemLists?lang=" + window.lang); const req = $.get("/custom/getItemLists?lang=" + window.lang);
req.done(async data => { req.done(async data => {
window.allQuestKeys = data.QuestKeys;
await dictPromise; await dictPromise;
document.querySelectorAll('[id^="datalist-"]').forEach(datalist => { document.querySelectorAll('[id^="datalist-"]').forEach(datalist => {
@ -728,7 +730,10 @@ function updateInventory() {
td.appendChild(a); td.appendChild(a);
} }
if (["Suits", "LongGuns", "Pistols", "Melee", "SpaceGuns", "SpaceMelee"].includes(category)) { if (
["Suits", "LongGuns", "Pistols", "Melee", "SpaceGuns", "SpaceMelee"].includes(category) ||
modularWeapons.includes(item.ItemType)
) {
const a = document.createElement("a"); const a = document.createElement("a");
a.href = "/webui/detailedView?productCategory=" + category + "&itemId=" + item.ItemId.$oid; a.href = "/webui/detailedView?productCategory=" + category + "&itemId=" + item.ItemId.$oid;
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M278.5 215.6L23 471c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l57-57h68c49.7 0 97.9-14.4 139-41c11.1-7.2 5.5-23-7.8-23c-5.1 0-9.2-4.1-9.2-9.2c0-4.1 2.7-7.6 6.5-8.8l81-24.3c2.5-.8 4.8-2.1 6.7-4l22.4-22.4c10.1-10.1 2.9-27.3-11.3-27.3l-32.2 0c-5.1 0-9.2-4.1-9.2-9.2c0-4.1 2.7-7.6 6.5-8.8l112-33.6c4-1.2 7.4-3.9 9.3-7.7C506.4 207.6 512 184.1 512 160c0-41-16.3-80.3-45.3-109.3l-5.5-5.5C432.3 16.3 393 0 352 0s-80.3 16.3-109.3 45.3L139 149C91 197 64 262.1 64 330v55.3L253.6 195.8c6.2-6.2 16.4-6.2 22.6 0c5.4 5.4 6.1 13.6 2.2 19.8z"/></svg>`; a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M278.5 215.6L23 471c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l57-57h68c49.7 0 97.9-14.4 139-41c11.1-7.2 5.5-23-7.8-23c-5.1 0-9.2-4.1-9.2-9.2c0-4.1 2.7-7.6 6.5-8.8l81-24.3c2.5-.8 4.8-2.1 6.7-4l22.4-22.4c10.1-10.1 2.9-27.3-11.3-27.3l-32.2 0c-5.1 0-9.2-4.1-9.2-9.2c0-4.1 2.7-7.6 6.5-8.8l112-33.6c4-1.2 7.4-3.9 9.3-7.7C506.4 207.6 512 184.1 512 160c0-41-16.3-80.3-45.3-109.3l-5.5-5.5C432.3 16.3 393 0 352 0s-80.3 16.3-109.3 45.3L139 149C91 197 64 262.1 64 330v55.3L253.6 195.8c6.2-6.2 16.4-6.2 22.6 0c5.4 5.4 6.1 13.6 2.2 19.8z"/></svg>`;
@ -879,6 +884,14 @@ function updateInventory() {
// Populate quests route // Populate quests route
document.getElementById("QuestKeys-list").innerHTML = ""; document.getElementById("QuestKeys-list").innerHTML = "";
window.allQuestKeys.forEach(questKey => {
if (!data.QuestKeys.some(x => x.ItemType == questKey.uniqueName)) {
const datalist = document.getElementById("datalist-QuestKeys");
if (!datalist.querySelector(`option[data-key="${questKey.uniqueName}"]`)) {
readdQuestKey(itemMap, questKey.uniqueName);
}
}
});
data.QuestKeys.forEach(item => { data.QuestKeys.forEach(item => {
const tr = document.createElement("tr"); const tr = document.createElement("tr");
tr.setAttribute("data-item-type", item.ItemType); tr.setAttribute("data-item-type", item.ItemType);
@ -972,10 +985,7 @@ function updateInventory() {
a.href = "#"; a.href = "#";
a.onclick = function (event) { a.onclick = function (event) {
event.preventDefault(); event.preventDefault();
const option = document.createElement("option"); readdQuestKey(itemMap, item.ItemType);
option.setAttribute("data-key", item.ItemType);
option.value = itemMap[item.ItemType]?.name ?? item.ItemType;
document.getElementById("datalist-QuestKeys").appendChild(option);
doQuestUpdate("deleteKey", item.ItemType); doQuestUpdate("deleteKey", item.ItemType);
}; };
a.title = loc("code_remove"); a.title = loc("code_remove");
@ -1226,6 +1236,35 @@ function updateInventory() {
document.getElementById("valenceBonus-procent").value = Math.round(buffValue * 1000) / 10; document.getElementById("valenceBonus-procent").value = Math.round(buffValue * 1000) / 10;
} }
} }
if (modularWeapons.includes(item.ItemType)) {
document.getElementById("modularParts-card").classList.remove("d-none");
const form = document.getElementById("modularParts-form");
form.innerHTML = "";
const requiredParts = getRequiredParts(category, item.ItemType);
requiredParts.forEach(modularPart => {
const input = document.createElement("input");
input.classList.add("form-control");
input.id = "detailedView-modularPart-" + modularPart;
input.setAttribute("list", "datalist-ModularParts-" + modularPart);
const datalist = document.getElementById("datalist-ModularParts-" + modularPart);
const options = Array.from(datalist.options);
input.value =
options.find(option => item.ModularParts.includes(option.getAttribute("data-key")))
?.value || "";
form.appendChild(input);
});
const changeButton = document.createElement("button");
changeButton.classList.add("btn");
changeButton.classList.add("btn-primary");
changeButton.type = "submit";
changeButton.setAttribute("data-loc", "cheats_changeButton");
changeButton.innerHTML = loc("cheats_changeButton");
form.appendChild(changeButton);
}
} else { } else {
single.loadRoute("/webui/inventory"); single.loadRoute("/webui/inventory");
} }
@ -1325,47 +1364,41 @@ function doAcquireEquipment(category) {
}); });
} }
function doAcquireModularEquipment(category, WeaponType) { function getRequiredParts(category, WeaponType) {
let requiredParts;
let Parts = [];
switch (category) { switch (category) {
case "HoverBoards": case "Hoverboards":
WeaponType = "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit"; return ["HB_DECK", "HB_ENGINE", "HB_FRONT", "HB_JET"];
requiredParts = ["HB_DECK", "HB_ENGINE", "HB_FRONT", "HB_JET"];
break;
case "OperatorAmps": case "OperatorAmps":
requiredParts = ["AMP_OCULUS", "AMP_CORE", "AMP_BRACE"]; return ["AMP_OCULUS", "AMP_CORE", "AMP_BRACE"];
break;
case "Melee": case "Melee":
requiredParts = ["BLADE", "HILT", "HILT_WEIGHT"]; return ["BLADE", "HILT", "HILT_WEIGHT"];
break;
case "LongGuns": case "LongGuns":
requiredParts = ["GUN_BARREL", "GUN_PRIMARY_HANDLE", "GUN_CLIP"]; return ["GUN_BARREL", "GUN_PRIMARY_HANDLE", "GUN_CLIP"];
break;
case "Pistols": case "Pistols":
requiredParts = ["GUN_BARREL", "GUN_SECONDARY_HANDLE", "GUN_CLIP"]; return ["GUN_BARREL", "GUN_SECONDARY_HANDLE", "GUN_CLIP"];
break;
case "MoaPets": case "MoaPets":
if (WeaponType == "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit") { return WeaponType === "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit"
requiredParts = ["MOA_ENGINE", "MOA_PAYLOAD", "MOA_HEAD", "MOA_LEG"]; ? ["MOA_ENGINE", "MOA_PAYLOAD", "MOA_HEAD", "MOA_LEG"]
} else { : ["ZANUKA_BODY", "ZANUKA_HEAD", "ZANUKA_LEG", "ZANUKA_TAIL"];
requiredParts = ["ZANUKA_BODY", "ZANUKA_HEAD", "ZANUKA_LEG", "ZANUKA_TAIL"];
} case "KubrowPets": {
break; return WeaponType.endsWith("InfestedCatbrowPetPowerSuit")
case "KubrowPets": ? ["CATBROW_ANTIGEN", "CATBROW_MUTAGEN"]
if ( : ["KUBROW_ANTIGEN", "KUBROW_MUTAGEN"];
[ }
"/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit"
].includes(WeaponType)
) {
requiredParts = ["CATBROW_ANTIGEN", "CATBROW_MUTAGEN"];
} else {
requiredParts = ["KUBROW_ANTIGEN", "KUBROW_MUTAGEN"];
}
break;
} }
}
function doAcquireModularEquipment(category, WeaponType) {
if (category === "Hoverboards") WeaponType = "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit";
const requiredParts = getRequiredParts(category, WeaponType);
let Parts = [];
requiredParts.forEach(part => { requiredParts.forEach(part => {
const partName = getKey(document.getElementById("acquire-type-" + category + "-" + part)); const partName = getKey(document.getElementById("acquire-type-" + category + "-" + part));
if (partName) { if (partName) {
@ -2189,6 +2222,8 @@ single.getRoute("#detailedView-route").on("beforeload", function () {
document.getElementById("detailedView-title").textContent = ""; document.getElementById("detailedView-title").textContent = "";
document.querySelector("#detailedView-route .text-body-secondary").textContent = ""; document.querySelector("#detailedView-route .text-body-secondary").textContent = "";
document.getElementById("archonShards-card").classList.add("d-none"); document.getElementById("archonShards-card").classList.add("d-none");
document.getElementById("modularParts-card").classList.add("d-none");
document.getElementById("modularParts-form").innerHTML = "";
document.getElementById("valenceBonus-card").classList.add("d-none"); document.getElementById("valenceBonus-card").classList.add("d-none");
if (window.didInitialInventoryUpdate) { if (window.didInitialInventoryUpdate) {
updateInventory(); updateInventory();
@ -2270,6 +2305,13 @@ function doAddCurrency(currency) {
}); });
} }
function readdQuestKey(itemMap, itemType) {
const option = document.createElement("option");
option.setAttribute("data-key", itemType);
option.value = itemMap[itemType]?.name ?? itemType;
document.getElementById("datalist-QuestKeys").appendChild(option);
}
function doQuestUpdate(operation, itemType) { function doQuestUpdate(operation, itemType) {
revalidateAuthz().then(() => { revalidateAuthz().then(() => {
$.post({ $.post({
@ -2339,22 +2381,10 @@ function handleModularSelection(category) {
modularFieldsZanuka.style.display = "none"; modularFieldsZanuka.style.display = "none";
} }
} else if (inventoryCategory === "KubrowPets") { } else if (inventoryCategory === "KubrowPets") {
if ( if (key.endsWith("InfestedCatbrowPetPowerSuit")) {
[
"/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit"
].includes(key)
) {
modularFieldsCatbrow.style.display = ""; modularFieldsCatbrow.style.display = "";
modularFieldsKubrow.style.display = "none"; modularFieldsKubrow.style.display = "none";
} else if ( } else if (key.endsWith("PredatorKubrowPetPowerSuit")) {
[
"/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit"
].includes(key)
) {
modularFieldsCatbrow.style.display = "none"; modularFieldsCatbrow.style.display = "none";
modularFieldsKubrow.style.display = ""; modularFieldsKubrow.style.display = "";
} else { } else {
@ -2793,3 +2823,38 @@ async function markAllAsRead() {
} }
toast(loc(any ? "code_succRelog" : "code_nothingToDo")); toast(loc(any ? "code_succRelog" : "code_nothingToDo"));
} }
function handleModularPartsChange(event) {
event.preventDefault();
const urlParams = new URLSearchParams(window.location.search);
const form = document.getElementById("modularParts-form");
const inputs = form.querySelectorAll("input");
const modularParts = [];
inputs.forEach(input => {
const key = getKey(input);
if (!getKey(input)) {
input.addClass("is-invalid");
} else {
modularParts.push(key);
}
});
revalidateAuthz().then(() => {
console.log({
category: urlParams.get("productCategory"),
oid: urlParams.get("itemId"),
modularParts
});
$.post({
url: "/custom/changeModularParts?" + window.authz,
contentType: "application/json",
data: JSON.stringify({
category: urlParams.get("productCategory"),
oid: urlParams.get("itemId"),
modularParts
})
}).then(function () {
updateInventory();
});
});
}

View File

@ -123,6 +123,7 @@ dict = {
detailedView_archonShardsDescription2: `Hinweis: Jede Archon-Scherbe benötigt beim Laden etwas Zeit, um angewendet zu werden.`, detailedView_archonShardsDescription2: `Hinweis: Jede Archon-Scherbe benötigt beim Laden etwas Zeit, um angewendet zu werden.`,
detailedView_valenceBonusLabel: `Valenz-Bonus`, detailedView_valenceBonusLabel: `Valenz-Bonus`,
detailedView_valenceBonusDescription: `[UNTRANSLATED] You can set or remove the Valence Bonus from your weapon.`, detailedView_valenceBonusDescription: `[UNTRANSLATED] You can set or remove the Valence Bonus from your weapon.`,
detailedView_modularPartsLabel: `[UNTRANSLATED] Change Modular Parts`,
mods_addRiven: `Riven hinzufügen`, mods_addRiven: `Riven hinzufügen`,
mods_fingerprint: `Fingerabdruck`, mods_fingerprint: `Fingerabdruck`,

View File

@ -122,6 +122,7 @@ dict = {
detailedView_archonShardsDescription2: `Note that each archon shard takes some time to be applied when loading in.`, detailedView_archonShardsDescription2: `Note that each archon shard takes some time to be applied when loading in.`,
detailedView_valenceBonusLabel: `Valence Bonus`, detailedView_valenceBonusLabel: `Valence Bonus`,
detailedView_valenceBonusDescription: `You can set or remove the Valence Bonus from your weapon.`, detailedView_valenceBonusDescription: `You can set or remove the Valence Bonus from your weapon.`,
detailedView_modularPartsLabel: `Change Modular Parts`,
mods_addRiven: `Add Riven`, mods_addRiven: `Add Riven`,
mods_fingerprint: `Fingerprint`, mods_fingerprint: `Fingerprint`,

View File

@ -4,7 +4,7 @@ dict = {
general_addButton: `Agregar`, general_addButton: `Agregar`,
general_setButton: `Establecer`, general_setButton: `Establecer`,
general_bulkActions: `Acciones masivas`, general_bulkActions: `Acciones masivas`,
general_loading: `[UNTRANSLATED] Loading...`, general_loading: `Cargando...`,
code_loginFail: `Error al iniciar sesión. Verifica el correo electrónico y la contraseña.`, code_loginFail: `Error al iniciar sesión. Verifica el correo electrónico y la contraseña.`,
code_regFail: `Error al registrar la cuenta. ¿Ya existe una cuenta con este correo?`, code_regFail: `Error al registrar la cuenta. ¿Ya existe una cuenta con este correo?`,
@ -45,8 +45,8 @@ dict = {
code_focusUnlocked: `¡Desbloqueadas |COUNT| nuevas escuelas de enfoque! Se necesita una actualización del inventario para reflejar los cambios en el juego. Visitar la navegación debería ser la forma más sencilla de activarlo.`, code_focusUnlocked: `¡Desbloqueadas |COUNT| nuevas escuelas de enfoque! Se necesita una actualización del inventario para reflejar los cambios en el juego. Visitar la navegación debería ser la forma más sencilla de activarlo.`,
code_addModsConfirm: `¿Estás seguro de que deseas agregar |COUNT| modificadores a tu cuenta?`, code_addModsConfirm: `¿Estás seguro de que deseas agregar |COUNT| modificadores a tu cuenta?`,
code_succImport: `Importación exitosa.`, code_succImport: `Importación exitosa.`,
code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`, code_succRelog: `Hecho. Ten en cuenta que deberás volver a iniciar sesión para ver los cambios en el juego.`,
code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`, code_nothingToDo: `Hecho. No había nada que hacer.`,
code_gild: `Refinar`, code_gild: `Refinar`,
code_moa: `Moa`, code_moa: `Moa`,
code_zanuka: `Sabueso`, code_zanuka: `Sabueso`,
@ -123,13 +123,14 @@ dict = {
detailedView_archonShardsDescription2: `Ten en cuenta que cada fragmento de archón tarda un poco en aplicarse al cargar`, detailedView_archonShardsDescription2: `Ten en cuenta que cada fragmento de archón tarda un poco en aplicarse al cargar`,
detailedView_valenceBonusLabel: `Bônus de Valência`, detailedView_valenceBonusLabel: `Bônus de Valência`,
detailedView_valenceBonusDescription: `Puedes establecer o quitar el bono de valencia de tu arma.`, detailedView_valenceBonusDescription: `Puedes establecer o quitar el bono de valencia de tu arma.`,
detailedView_modularPartsLabel: `[UNTRANSLATED] Change Modular Parts`,
mods_addRiven: `Agregar Agrietado`, mods_addRiven: `Agregar Agrietado`,
mods_fingerprint: `Huella digital`, mods_fingerprint: `Huella digital`,
mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`, mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`,
mods_rivens: `Agrietados`, mods_rivens: `Agrietados`,
mods_mods: `Mods`, mods_mods: `Mods`,
mods_addMax: `[UNTRANSLATED] Add Maxed`, mods_addMax: `Agregar al máximo`,
mods_addMissingUnrankedMods: `Agregar mods sin rango faltantes`, mods_addMissingUnrankedMods: `Agregar mods sin rango faltantes`,
mods_removeUnranked: `Quitar mods sin rango`, mods_removeUnranked: `Quitar mods sin rango`,
mods_addMissingMaxRankMods: `Agregar mods de rango máximo faltantes`, mods_addMissingMaxRankMods: `Agregar mods de rango máximo faltantes`,
@ -185,13 +186,13 @@ dict = {
cheats_noDojoResearchTime: `Sin tiempo de investigación del dojo`, cheats_noDojoResearchTime: `Sin tiempo de investigación del dojo`,
cheats_fastClanAscension: `Ascenso rápido del clan`, cheats_fastClanAscension: `Ascenso rápido del clan`,
cheats_missionsCanGiveAllRelics: `Las misiones pueden otorgar todas las reliquias`, cheats_missionsCanGiveAllRelics: `Las misiones pueden otorgar todas las reliquias`,
cheats_exceptionalRelicsAlwaysGiveBronzeReward: `[UNTRANSLATED] Exceptional Relics Always Give Bronze Reward`, cheats_exceptionalRelicsAlwaysGiveBronzeReward: `Las reliquias excepcionales siempre otorgan recompensa de bronce`,
cheats_flawlessRelicsAlwaysGiveSilverReward: `[UNTRANSLATED] Flawless Relics Always Give Silver Reward`, cheats_flawlessRelicsAlwaysGiveSilverReward: `Las reliquias impecables siempre otorgan recompensa de plata`,
cheats_radiantRelicsAlwaysGiveGoldReward: `[UNTRANSLATED] Radiant Relics Always Give Gold Reward`, cheats_radiantRelicsAlwaysGiveGoldReward: `Las reliquias radiantes siempre otorgan recompensa de oro`,
cheats_unlockAllSimarisResearchEntries: `Desbloquear todas las entradas de investigación de Simaris`, cheats_unlockAllSimarisResearchEntries: `Desbloquear todas las entradas de investigación de Simaris`,
cheats_disableDailyTribute: `Desactivar tributo diario`, cheats_disableDailyTribute: `Desactivar tributo diario`,
cheats_spoofMasteryRank: `Rango de maestría simulado (-1 para desactivar)`, cheats_spoofMasteryRank: `Rango de maestría simulado (-1 para desactivar)`,
cheats_relicRewardItemCountMultiplier: `[UNTRANSLATED] Relic Reward Item Count Multiplier`, cheats_relicRewardItemCountMultiplier: `Multiplicador de cantidad de recompensas de reliquia`,
cheats_nightwaveStandingMultiplier: `Multiplicador de Reputación de Onda Nocturna`, cheats_nightwaveStandingMultiplier: `Multiplicador de Reputación de Onda Nocturna`,
cheats_save: `Guardar`, cheats_save: `Guardar`,
cheats_account: `Cuenta`, cheats_account: `Cuenta`,
@ -202,7 +203,7 @@ dict = {
cheats_changeSupportedSyndicate: `Sindicatos disponibles`, cheats_changeSupportedSyndicate: `Sindicatos disponibles`,
cheats_changeButton: `Cambiar`, cheats_changeButton: `Cambiar`,
cheats_none: `Ninguno`, cheats_none: `Ninguno`,
cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`, cheats_markAllAsRead: `Marcar bandeja de entrada como leída`,
worldState: `Estado del mundo`, worldState: `Estado del mundo`,
worldState_creditBoost: `Potenciador de Créditos`, worldState_creditBoost: `Potenciador de Créditos`,
@ -249,8 +250,8 @@ dict = {
worldState_allAtOnceSteelPath: `Todo a la vez, Camino de Acero`, worldState_allAtOnceSteelPath: `Todo a la vez, Camino de Acero`,
worldState_theCircuitOverride: `Cambio del Circuito`, worldState_theCircuitOverride: `Cambio del Circuito`,
worldState_darvoStockMultiplier: `Multiplicador de stock de Darvo`, worldState_darvoStockMultiplier: `Multiplicador de stock de Darvo`,
worldState_varziaFullyStocked: `[UNTRANSLATED] Varzia Fully Stocked`, worldState_varziaFullyStocked: `Varzia con stock completo`,
worldState_varziaOverride: `[UNTRANSLATED] Varzia Rotation Override`, worldState_varziaOverride: `Cambio en rotación de Varzia`,
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_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`, import_submit: `Enviar`,
@ -303,10 +304,10 @@ dict = {
upgrade_OnExecutionTerrify: `50% de probabilidad de que enemigos en un radio de 15m entren en pánico por 8s tras una ejecución`, 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_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_OnExecutionBlind: `Ciega a los enemigos en un radio de 18m tras una ejecución`,
upgrade_OnExecutionDrainPower: `[UNTRANSLATED] Next ability cast gains +50% Ability Strength on Mercy`, upgrade_OnExecutionDrainPower: `La próxima habilidad usada gana +50% de fuerza al realizar un remate (Mercy)`,
upgrade_OnHackSprintSpeed: `+75% de velocidad de carrera durante 15s después de hackear`, upgrade_OnHackSprintSpeed: `+75% de velocidad de carrera durante 15s después de hackear`,
upgrade_SwiftExecute: `[UNTRANSLATED] +50% Mercy Kill Speed`, upgrade_SwiftExecute: `+50% de velocidad al ejecutar remates (Mercy)`,
upgrade_OnHackInvis: `[UNTRANSLATED] Invisible for 15 seconds after Hacking`, upgrade_OnHackInvis: `Invisible durante 15 segundos después de hackear`,
damageType_Electricity: `Eletricidade`, damageType_Electricity: `Eletricidade`,
damageType_Fire: `Ígneo`, damageType_Fire: `Ígneo`,

View File

@ -123,6 +123,7 @@ dict = {
detailedView_archonShardsDescription2: `Un délai sera présent entre l'application des éclats et le chargement en jeu.`, detailedView_archonShardsDescription2: `Un délai sera présent entre l'application des éclats et le chargement en jeu.`,
detailedView_valenceBonusLabel: `Bonus de Valence`, detailedView_valenceBonusLabel: `Bonus de Valence`,
detailedView_valenceBonusDescription: `[UNTRANSLATED] You can set or remove the Valence Bonus from your weapon.`, detailedView_valenceBonusDescription: `[UNTRANSLATED] You can set or remove the Valence Bonus from your weapon.`,
detailedView_modularPartsLabel: `[UNTRANSLATED] Change Modular Parts`,
mods_addRiven: `Ajouter un riven`, mods_addRiven: `Ajouter un riven`,
mods_fingerprint: `Empreinte`, mods_fingerprint: `Empreinte`,

View File

@ -123,6 +123,7 @@ dict = {
detailedView_archonShardsDescription2: `Обратите внимание: каждый фрагмент архонта применяется с задержкой при загрузке.`, detailedView_archonShardsDescription2: `Обратите внимание: каждый фрагмент архонта применяется с задержкой при загрузке.`,
detailedView_valenceBonusLabel: `Бонус Валентности`, detailedView_valenceBonusLabel: `Бонус Валентности`,
detailedView_valenceBonusDescription: `Вы можете установить или убрать бонус валентности с вашего оружия.`, detailedView_valenceBonusDescription: `Вы можете установить или убрать бонус валентности с вашего оружия.`,
detailedView_modularPartsLabel: `Изменить Модульные Части`,
mods_addRiven: `Добавить Мод Разлома`, mods_addRiven: `Добавить Мод Разлома`,
mods_fingerprint: `Отпечаток`, mods_fingerprint: `Отпечаток`,

View File

@ -4,7 +4,7 @@ dict = {
general_addButton: `添加`, general_addButton: `添加`,
general_setButton: `设置`, general_setButton: `设置`,
general_bulkActions: `批量操作`, general_bulkActions: `批量操作`,
general_loading: `[UNTRANSLATED] Loading...`, general_loading: `加载中...`,
code_loginFail: `登录失败.请检查邮箱和密码.`, code_loginFail: `登录失败.请检查邮箱和密码.`,
code_regFail: `注册失败.账号已存在.`, code_regFail: `注册失败.账号已存在.`,
@ -45,8 +45,8 @@ dict = {
code_focusUnlocked: `已解锁|COUNT|个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新.`, code_focusUnlocked: `已解锁|COUNT|个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新.`,
code_addModsConfirm: `确定要向账户添加|COUNT|张MOD吗?`, code_addModsConfirm: `确定要向账户添加|COUNT|张MOD吗?`,
code_succImport: `导入成功。`, code_succImport: `导入成功。`,
code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`, code_succRelog: `完成. 需要重新登录游戏才能看到变化.`,
code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`, code_nothingToDo: `完成. 没有可执行的操作.`,
code_gild: `镀金`, code_gild: `镀金`,
code_moa: `恐鸟`, code_moa: `恐鸟`,
code_zanuka: `猎犬`, code_zanuka: `猎犬`,
@ -123,13 +123,14 @@ dict = {
detailedView_archonShardsDescription2: `请注意,在加载时,每个执政官源力石都需要一定的时间来生效。`, detailedView_archonShardsDescription2: `请注意,在加载时,每个执政官源力石都需要一定的时间来生效。`,
detailedView_valenceBonusLabel: `效价加成`, detailedView_valenceBonusLabel: `效价加成`,
detailedView_valenceBonusDescription: `您可以设置或移除武器上的效价加成.`, detailedView_valenceBonusDescription: `您可以设置或移除武器上的效价加成.`,
detailedView_modularPartsLabel: `[UNTRANSLATED] Change Modular Parts`,
mods_addRiven: `添加裂罅MOD`, mods_addRiven: `添加裂罅MOD`,
mods_fingerprint: `印记`, mods_fingerprint: `印记`,
mods_fingerprintHelp: `需要印记相关的帮助?`, mods_fingerprintHelp: `需要印记相关的帮助?`,
mods_rivens: `裂罅MOD`, mods_rivens: `裂罅MOD`,
mods_mods: `Mods`, mods_mods: `Mods`,
mods_addMax: `添加(满级)`, mods_addMax: `满级添加`,
mods_addMissingUnrankedMods: `添加所有缺失的Mods`, mods_addMissingUnrankedMods: `添加所有缺失的Mods`,
mods_removeUnranked: `删除所有未升级的Mods`, mods_removeUnranked: `删除所有未升级的Mods`,
mods_addMissingMaxRankMods: `添加所有缺失的满级Mods`, mods_addMissingMaxRankMods: `添加所有缺失的满级Mods`,
@ -202,7 +203,7 @@ dict = {
cheats_changeSupportedSyndicate: `支持的集团`, cheats_changeSupportedSyndicate: `支持的集团`,
cheats_changeButton: `更改`, cheats_changeButton: `更改`,
cheats_none: ``, cheats_none: ``,
cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`, cheats_markAllAsRead: `收件箱全部标记为已读`,
worldState: `世界状态配置`, worldState: `世界状态配置`,
worldState_creditBoost: `现金加成`, worldState_creditBoost: `现金加成`,