chore(webui): improving inconsistent, long string #2344
@ -14,7 +14,8 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
 | 
					- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
 | 
				
			||||||
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
 | 
					- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
 | 
				
			||||||
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
 | 
					- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.
 | 
				
			||||||
 | 
					- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
 | 
				
			||||||
- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
 | 
					- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
 | 
				
			||||||
  - `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
 | 
					  - `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
 | 
				
			||||||
  - `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
 | 
					  - `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
 | 
				
			||||||
 | 
				
			|||||||
@ -55,7 +55,8 @@
 | 
				
			|||||||
    "affinityBoost": false,
 | 
					    "affinityBoost": false,
 | 
				
			||||||
    "resourceBoost": false,
 | 
					    "resourceBoost": false,
 | 
				
			||||||
    "starDays": true,
 | 
					    "starDays": true,
 | 
				
			||||||
    "lockTime": 0,
 | 
					    "eidolonOverride": "",
 | 
				
			||||||
 | 
					    "vallisOverride": "",
 | 
				
			||||||
    "nightwaveOverride": ""
 | 
					    "nightwaveOverride": ""
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -18,7 +18,7 @@
 | 
				
			|||||||
        "morgan": "^1.10.0",
 | 
					        "morgan": "^1.10.0",
 | 
				
			||||||
        "ncp": "^2.0.0",
 | 
					        "ncp": "^2.0.0",
 | 
				
			||||||
        "typescript": "^5.5",
 | 
					        "typescript": "^5.5",
 | 
				
			||||||
        "warframe-public-export-plus": "^0.5.65",
 | 
					        "warframe-public-export-plus": "^0.5.66",
 | 
				
			||||||
        "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"
 | 
				
			||||||
@ -3814,9 +3814,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/warframe-public-export-plus": {
 | 
					    "node_modules/warframe-public-export-plus": {
 | 
				
			||||||
      "version": "0.5.65",
 | 
					      "version": "0.5.66",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.65.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.66.tgz",
 | 
				
			||||||
      "integrity": "sha512-y/HN61lE5g8gx0Giutdl/jzQnQmw1u2uI0BiwKVW341nf42sKWQPsKsCVTL5x9MIDYyRCbFsMU+PazKC7byMdg=="
 | 
					      "integrity": "sha512-AU7XQA96OfYrLm2RioCwDjjdI3IrsmUiqebXyE+bpM0iST+4x/NHu8LTRT4Oygfo/2OBtDYhib7G6re0EeAe5g=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/warframe-riven-info": {
 | 
					    "node_modules/warframe-riven-info": {
 | 
				
			||||||
      "version": "0.1.2",
 | 
					      "version": "0.1.2",
 | 
				
			||||||
 | 
				
			|||||||
@ -25,7 +25,7 @@
 | 
				
			|||||||
    "morgan": "^1.10.0",
 | 
					    "morgan": "^1.10.0",
 | 
				
			||||||
    "ncp": "^2.0.0",
 | 
					    "ncp": "^2.0.0",
 | 
				
			||||||
    "typescript": "^5.5",
 | 
					    "typescript": "^5.5",
 | 
				
			||||||
    "warframe-public-export-plus": "^0.5.65",
 | 
					    "warframe-public-export-plus": "^0.5.66",
 | 
				
			||||||
    "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"
 | 
				
			||||||
 | 
				
			|||||||
@ -118,7 +118,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
 | 
				
			|||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusIncubating;
 | 
					            pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
 | 
				
			||||||
        } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
					        } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
				
			||||||
            InventoryChanges = {
 | 
					            InventoryChanges = {
 | 
				
			||||||
                ...InventoryChanges,
 | 
					                ...InventoryChanges,
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ import { RequestHandler } from "express";
 | 
				
			|||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
 | 
					import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
 | 
				
			||||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
					import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
 | 
				
			||||||
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
					import { addMiscItem, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
				
			||||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
					import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
import { toStoreItem } from "@/src/services/itemDataService";
 | 
					import { toStoreItem } from "@/src/services/itemDataService";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
@ -18,80 +18,83 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
 | 
				
			|||||||
        syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1];
 | 
					        syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const level = data.SacrificeLevel - (syndicate.Title ?? 0);
 | 
					    const oldLevel = syndicate.Title ?? 0;
 | 
				
			||||||
 | 
					    const levelIncrease = data.SacrificeLevel - oldLevel;
 | 
				
			||||||
 | 
					    if (levelIncrease < 0) {
 | 
				
			||||||
 | 
					        throw new Error(`syndicate sacrifice can not decrease level`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (levelIncrease > 1 && !data.AllowMultiple) {
 | 
				
			||||||
 | 
					        throw new Error(`desired syndicate level is an increase of ${levelIncrease}, max. allowed increase is 1`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res: ISyndicateSacrificeResponse = {
 | 
					    const res: ISyndicateSacrificeResponse = {
 | 
				
			||||||
        AffiliationTag: data.AffiliationTag,
 | 
					        AffiliationTag: data.AffiliationTag,
 | 
				
			||||||
        InventoryChanges: {},
 | 
					        InventoryChanges: {},
 | 
				
			||||||
        Level: data.SacrificeLevel,
 | 
					        Level: data.SacrificeLevel,
 | 
				
			||||||
        LevelIncrease: level <= 0 ? 1 : level,
 | 
					        LevelIncrease: levelIncrease,
 | 
				
			||||||
        NewEpisodeReward: false
 | 
					        NewEpisodeReward: false
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Process sacrifices and rewards for every level we're reaching
 | 
				
			||||||
    const manifest = ExportSyndicates[data.AffiliationTag];
 | 
					    const manifest = ExportSyndicates[data.AffiliationTag];
 | 
				
			||||||
    let sacrifice: ISyndicateSacrifice | undefined;
 | 
					    for (let level = oldLevel + levelIncrease; level <= data.SacrificeLevel; ++level) {
 | 
				
			||||||
    let reward: string | undefined;
 | 
					        let sacrifice: ISyndicateSacrifice | undefined;
 | 
				
			||||||
    if (data.SacrificeLevel == 0) {
 | 
					        if (level == 0) {
 | 
				
			||||||
        sacrifice = manifest.initiationSacrifice;
 | 
					            sacrifice = manifest.initiationSacrifice;
 | 
				
			||||||
        reward = manifest.initiationReward;
 | 
					            if (manifest.initiationReward) {
 | 
				
			||||||
        syndicate.Initiated = true;
 | 
					                combineInventoryChanges(
 | 
				
			||||||
    } else {
 | 
					                    res.InventoryChanges,
 | 
				
			||||||
        sacrifice = manifest.titles?.find(x => x.level == data.SacrificeLevel)?.sacrifice;
 | 
					                    (await handleStoreItemAcquisition(manifest.initiationReward, inventory)).InventoryChanges
 | 
				
			||||||
    }
 | 
					                );
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (sacrifice) {
 | 
					 | 
				
			||||||
        res.InventoryChanges = { ...updateCurrency(inventory, sacrifice.credits, false) };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const miscItemChanges = sacrifice.items.map(x => ({
 | 
					 | 
				
			||||||
            ItemType: x.ItemType,
 | 
					 | 
				
			||||||
            ItemCount: x.ItemCount * -1
 | 
					 | 
				
			||||||
        }));
 | 
					 | 
				
			||||||
        addMiscItems(inventory, miscItemChanges);
 | 
					 | 
				
			||||||
        res.InventoryChanges.MiscItems = miscItemChanges;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    syndicate.Title ??= 0;
 | 
					 | 
				
			||||||
    syndicate.Title += 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (reward) {
 | 
					 | 
				
			||||||
        combineInventoryChanges(
 | 
					 | 
				
			||||||
            res.InventoryChanges,
 | 
					 | 
				
			||||||
            (await handleStoreItemAcquisition(reward, inventory)).InventoryChanges
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Quacks like a nightwave syndicate?
 | 
					 | 
				
			||||||
    if (manifest.dailyChallenges) {
 | 
					 | 
				
			||||||
        const title = manifest.titles!.find(x => x.level == syndicate.Title);
 | 
					 | 
				
			||||||
        if (title) {
 | 
					 | 
				
			||||||
            res.NewEpisodeReward = true;
 | 
					 | 
				
			||||||
            let rewardType: string;
 | 
					 | 
				
			||||||
            let rewardCount: number;
 | 
					 | 
				
			||||||
            if (title.storeItemReward) {
 | 
					 | 
				
			||||||
                rewardType = title.storeItemReward;
 | 
					 | 
				
			||||||
                rewardCount = 1;
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                rewardType = toStoreItem(title.reward!.ItemType);
 | 
					 | 
				
			||||||
                rewardCount = title.reward!.ItemCount;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount))
 | 
					            syndicate.Initiated = true;
 | 
				
			||||||
                .InventoryChanges;
 | 
					        } else {
 | 
				
			||||||
            if (Object.keys(rewardInventoryChanges).length == 0) {
 | 
					            sacrifice = manifest.titles?.find(x => x.level == level)?.sacrifice;
 | 
				
			||||||
                logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
 | 
					 | 
				
			||||||
                const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType;
 | 
					 | 
				
			||||||
                rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }];
 | 
					 | 
				
			||||||
                addMiscItems(inventory, rewardInventoryChanges.MiscItems);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    } else {
 | 
					
 | 
				
			||||||
        if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
 | 
					        if (sacrifice) {
 | 
				
			||||||
            syndicate.FreeFavorsEarned ??= [];
 | 
					            updateCurrency(inventory, sacrifice.credits, false, res.InventoryChanges);
 | 
				
			||||||
            if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
 | 
					
 | 
				
			||||||
                syndicate.FreeFavorsEarned.push(syndicate.Title);
 | 
					            for (const item of sacrifice.items) {
 | 
				
			||||||
 | 
					                addMiscItem(inventory, item.ItemType, item.ItemCount * -1, res.InventoryChanges);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Quacks like a nightwave syndicate?
 | 
				
			||||||
 | 
					        if (manifest.dailyChallenges) {
 | 
				
			||||||
 | 
					            const title = manifest.titles!.find(x => x.level == level);
 | 
				
			||||||
 | 
					            if (title) {
 | 
				
			||||||
 | 
					                res.NewEpisodeReward = true;
 | 
				
			||||||
 | 
					                let rewardType: string;
 | 
				
			||||||
 | 
					                let rewardCount: number;
 | 
				
			||||||
 | 
					                if (title.storeItemReward) {
 | 
				
			||||||
 | 
					                    rewardType = title.storeItemReward;
 | 
				
			||||||
 | 
					                    rewardCount = 1;
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    rewardType = toStoreItem(title.reward!.ItemType);
 | 
				
			||||||
 | 
					                    rewardCount = title.reward!.ItemCount;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount))
 | 
				
			||||||
 | 
					                    .InventoryChanges;
 | 
				
			||||||
 | 
					                if (Object.keys(rewardInventoryChanges).length == 0) {
 | 
				
			||||||
 | 
					                    logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
 | 
				
			||||||
 | 
					                    const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType;
 | 
				
			||||||
 | 
					                    addMiscItem(inventory, nightwaveCredsItemType, 50, rewardInventoryChanges);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            if (level > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == level)) {
 | 
				
			||||||
 | 
					                syndicate.FreeFavorsEarned ??= [];
 | 
				
			||||||
 | 
					                if (!syndicate.FreeFavorsEarned.includes(level)) {
 | 
				
			||||||
 | 
					                    syndicate.FreeFavorsEarned.push(level);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Commit
 | 
				
			||||||
 | 
					    syndicate.Title = data.SacrificeLevel;
 | 
				
			||||||
    await inventory.save();
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    response.json(res);
 | 
					    response.json(res);
 | 
				
			||||||
 | 
				
			|||||||
@ -35,6 +35,17 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
 | 
				
			|||||||
        inventory.PlayerLevel += 1;
 | 
					        inventory.PlayerLevel += 1;
 | 
				
			||||||
        inventory.TradesRemaining += 1;
 | 
					        inventory.TradesRemaining += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (inventory.PlayerLevel == 2) {
 | 
				
			||||||
 | 
					            await createMessage(accountId, [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    sndr: "/Lotus/Language/Game/Maroo",
 | 
				
			||||||
 | 
					                    msg: "/Lotus/Language/Clan/MarooClanSearchDesc",
 | 
				
			||||||
 | 
					                    sub: "/Lotus/Language/Clan/MarooClanSearchTitle",
 | 
				
			||||||
 | 
					                    icon: "/Lotus/Interface/Icons/Npcs/Maroo.png"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await createMessage(accountId, [
 | 
					        await createMessage(accountId, [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
					                sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,7 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService"
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    ExportArcanes,
 | 
					    ExportArcanes,
 | 
				
			||||||
    ExportAvionics,
 | 
					    ExportAvionics,
 | 
				
			||||||
 | 
					    ExportBoosters,
 | 
				
			||||||
    ExportCustoms,
 | 
					    ExportCustoms,
 | 
				
			||||||
    ExportDrones,
 | 
					    ExportDrones,
 | 
				
			||||||
    ExportGear,
 | 
					    ExportGear,
 | 
				
			||||||
@ -55,6 +56,7 @@ interface ItemLists {
 | 
				
			|||||||
    KubrowPets: ListedItem[];
 | 
					    KubrowPets: ListedItem[];
 | 
				
			||||||
    EvolutionProgress: ListedItem[];
 | 
					    EvolutionProgress: ListedItem[];
 | 
				
			||||||
    mods: ListedItem[];
 | 
					    mods: ListedItem[];
 | 
				
			||||||
 | 
					    Boosters: ListedItem[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const relicQualitySuffixes: Record<TRelicQuality, string> = {
 | 
					const relicQualitySuffixes: Record<TRelicQuality, string> = {
 | 
				
			||||||
@ -86,7 +88,8 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
				
			|||||||
        QuestKeys: [],
 | 
					        QuestKeys: [],
 | 
				
			||||||
        KubrowPets: [],
 | 
					        KubrowPets: [],
 | 
				
			||||||
        EvolutionProgress: [],
 | 
					        EvolutionProgress: [],
 | 
				
			||||||
        mods: []
 | 
					        mods: [],
 | 
				
			||||||
 | 
					        Boosters: []
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
 | 
					    for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
 | 
				
			||||||
        res[item.productCategory].push({
 | 
					        res[item.productCategory].push({
 | 
				
			||||||
@ -296,6 +299,13 @@ const getItemListsController: RequestHandler = (req, response) => {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const item of Object.values(ExportBoosters)) {
 | 
				
			||||||
 | 
					        res.Boosters.push({
 | 
				
			||||||
 | 
					            uniqueName: item.typeName,
 | 
				
			||||||
 | 
					            name: getString(item.name, lang)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    response.json(res);
 | 
					    response.json(res);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										45
									
								
								src/controllers/custom/setBoosterController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/controllers/custom/setBoosterController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
 | 
					import { getInventory } from "@/src/services/inventoryService";
 | 
				
			||||||
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
 | 
					import { ExportBoosters } from "warframe-public-export-plus";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const I32_MAX = 0x7fffffff;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const setBoosterController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const requests = req.body as { ItemType: string; ExpiryDate: number }[];
 | 
				
			||||||
 | 
					    const inventory = await getInventory(accountId, "Boosters");
 | 
				
			||||||
 | 
					    const boosters = inventory.Boosters;
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					        requests.some(request => {
 | 
				
			||||||
 | 
					            if (typeof request.ItemType !== "string") return true;
 | 
				
			||||||
 | 
					            if (Object.entries(ExportBoosters).find(([_, item]) => item.typeName === request.ItemType) === undefined)
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            if (typeof request.ExpiryDate !== "number") return true;
 | 
				
			||||||
 | 
					            if (request.ExpiryDate < 0 || request.ExpiryDate > I32_MAX) return true;
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        res.status(400).send("Invalid ItemType provided.");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const now = Math.floor(Date.now() / 1000);
 | 
				
			||||||
 | 
					    for (const { ItemType, ExpiryDate } of requests) {
 | 
				
			||||||
 | 
					        if (ExpiryDate < now) {
 | 
				
			||||||
 | 
					            // remove expired boosters
 | 
				
			||||||
 | 
					            const index = boosters.findIndex(item => item.ItemType === ItemType);
 | 
				
			||||||
 | 
					            if (index !== -1) {
 | 
				
			||||||
 | 
					                boosters.splice(index, 1);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            const boosterItem = boosters.find(item => item.ItemType === ItemType);
 | 
				
			||||||
 | 
					            if (boosterItem) {
 | 
				
			||||||
 | 
					                boosterItem.ExpiryDate = ExpiryDate;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                boosters.push({ ItemType, ExpiryDate });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
					    res.end();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -23,6 +23,7 @@ import { setEvolutionProgressController } from "@/src/controllers/custom/setEvol
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController";
 | 
					import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController";
 | 
				
			||||||
import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController";
 | 
					import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController";
 | 
				
			||||||
 | 
					import { setBoosterController } from "../controllers/custom/setBoosterController";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const customRouter = express.Router();
 | 
					const customRouter = express.Router();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -46,6 +47,7 @@ customRouter.post("/addXp", addXpController);
 | 
				
			|||||||
customRouter.post("/import", importController);
 | 
					customRouter.post("/import", importController);
 | 
				
			||||||
customRouter.post("/manageQuests", manageQuestsController);
 | 
					customRouter.post("/manageQuests", manageQuestsController);
 | 
				
			||||||
customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
 | 
					customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
 | 
				
			||||||
 | 
					customRouter.post("/setBooster", setBoosterController);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
customRouter.get("/config", getConfigDataController);
 | 
					customRouter.get("/config", getConfigDataController);
 | 
				
			||||||
customRouter.post("/config", updateConfigDataController);
 | 
					customRouter.post("/config", updateConfigDataController);
 | 
				
			||||||
 | 
				
			|||||||
@ -61,9 +61,11 @@ interface IConfig {
 | 
				
			|||||||
        affinityBoost?: boolean;
 | 
					        affinityBoost?: boolean;
 | 
				
			||||||
        resourceBoost?: boolean;
 | 
					        resourceBoost?: boolean;
 | 
				
			||||||
        starDays?: boolean;
 | 
					        starDays?: boolean;
 | 
				
			||||||
        lockTime?: number;
 | 
					        eidolonOverride?: string;
 | 
				
			||||||
 | 
					        vallisOverride?: string;
 | 
				
			||||||
        nightwaveOverride?: string;
 | 
					        nightwaveOverride?: string;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					    nightwaveStandingMultiplier?: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const configPath = path.join(repoDir, "config.json");
 | 
					export const configPath = path.join(repoDir, "config.json");
 | 
				
			||||||
 | 
				
			|||||||
@ -1525,7 +1525,8 @@ export const applyClientEquipmentUpdates = (
 | 
				
			|||||||
    gearArray.forEach(({ ItemId, XP, InfestationDate }) => {
 | 
					    gearArray.forEach(({ ItemId, XP, InfestationDate }) => {
 | 
				
			||||||
        const item = category.id(fromOid(ItemId));
 | 
					        const item = category.id(fromOid(ItemId));
 | 
				
			||||||
        if (!item) {
 | 
					        if (!item) {
 | 
				
			||||||
            throw new Error(`No item with id ${fromOid(ItemId)} in ${categoryName}`);
 | 
					            logger.warn(`Skipping unknown ${categoryName} item: id ${fromOid(ItemId)} not found`);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (XP) {
 | 
					        if (XP) {
 | 
				
			||||||
@ -1772,13 +1773,14 @@ export const addChallenges = (
 | 
				
			|||||||
                            }) - 1
 | 
					                            }) - 1
 | 
				
			||||||
                        ];
 | 
					                        ];
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                affiliation.Standing += meta.standing!;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const standingToAdd = meta.standing! * (config.nightwaveStandingMultiplier ?? 1);
 | 
				
			||||||
 | 
					                affiliation.Standing += standingToAdd;
 | 
				
			||||||
                if (affiliationMods.length == 0) {
 | 
					                if (affiliationMods.length == 0) {
 | 
				
			||||||
                    affiliationMods.push({ Tag: nightwaveSyndicateTag });
 | 
					                    affiliationMods.push({ Tag: nightwaveSyndicateTag });
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                affiliationMods[0].Standing ??= 0;
 | 
					                affiliationMods[0].Standing ??= 0;
 | 
				
			||||||
                affiliationMods[0].Standing += meta.standing!;
 | 
					                affiliationMods[0].Standing += standingToAdd;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -233,7 +233,7 @@ export const isStoreItem = (type: string): boolean => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const toStoreItem = (type: string): string => {
 | 
					export const toStoreItem = (type: string): string => {
 | 
				
			||||||
    if (type.startsWith("/Lotus/Types/StoreItems/Boosters/")) {
 | 
					    if (type.startsWith("/Lotus/Types/Boosters/")) {
 | 
				
			||||||
        const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type);
 | 
					        const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type);
 | 
				
			||||||
        if (boosterEntry) {
 | 
					        if (boosterEntry) {
 | 
				
			||||||
            return boosterEntry[0];
 | 
					            return boosterEntry[0];
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,7 @@ import {
 | 
				
			|||||||
    IWorldState
 | 
					    IWorldState
 | 
				
			||||||
} from "../types/worldStateTypes";
 | 
					} from "../types/worldStateTypes";
 | 
				
			||||||
import { version_compare } from "../helpers/inventoryHelpers";
 | 
					import { version_compare } from "../helpers/inventoryHelpers";
 | 
				
			||||||
 | 
					import { logger } from "../utils/logger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sortieBosses = [
 | 
					const sortieBosses = [
 | 
				
			||||||
    "SORTIE_BOSS_HYENA",
 | 
					    "SORTIE_BOSS_HYENA",
 | 
				
			||||||
@ -938,8 +939,61 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const doesTimeSatsifyConstraints = (timeSecs: number): boolean => {
 | 
				
			||||||
 | 
					    if (config.worldState?.eidolonOverride) {
 | 
				
			||||||
 | 
					        const eidolonEpoch = 1391992660;
 | 
				
			||||||
 | 
					        const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000);
 | 
				
			||||||
 | 
					        const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000;
 | 
				
			||||||
 | 
					        const eidolonCycleEnd = eidolonCycleStart + 9000;
 | 
				
			||||||
 | 
					        const eidolonCycleNightStart = eidolonCycleEnd - 3000;
 | 
				
			||||||
 | 
					        if (config.worldState.eidolonOverride == "day") {
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                //timeSecs < eidolonCycleStart ||
 | 
				
			||||||
 | 
					                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleNightStart * 1000)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                timeSecs < eidolonCycleNightStart ||
 | 
				
			||||||
 | 
					                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleEnd * 1000)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (config.worldState?.vallisOverride) {
 | 
				
			||||||
 | 
					        const vallisEpoch = 1541837628;
 | 
				
			||||||
 | 
					        const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
 | 
				
			||||||
 | 
					        const vallisCycleStart = vallisEpoch + vallisCycle * 1600;
 | 
				
			||||||
 | 
					        const vallisCycleEnd = vallisCycleStart + 1600;
 | 
				
			||||||
 | 
					        const vallisCycleColdStart = vallisCycleStart + 400;
 | 
				
			||||||
 | 
					        if (config.worldState.vallisOverride == "cold") {
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                timeSecs < vallisCycleColdStart ||
 | 
				
			||||||
 | 
					                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleEnd * 1000)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                //timeSecs < vallisCycleStart ||
 | 
				
			||||||
 | 
					                isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleColdStart * 1000)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
					export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
				
			||||||
    const timeSecs = config.worldState?.lockTime || Math.round(Date.now() / 1000);
 | 
					    let timeSecs = Math.round(Date.now() / 1000);
 | 
				
			||||||
 | 
					    while (!doesTimeSatsifyConstraints(timeSecs)) {
 | 
				
			||||||
 | 
					        timeSecs -= 60;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    const timeMs = timeSecs * 1000;
 | 
					    const timeMs = timeSecs * 1000;
 | 
				
			||||||
    const day = Math.trunc((timeMs - EPOCH) / 86400000);
 | 
					    const day = Math.trunc((timeMs - EPOCH) / 86400000);
 | 
				
			||||||
    const week = Math.trunc(day / 7);
 | 
					    const week = Math.trunc(day / 7);
 | 
				
			||||||
@ -1275,7 +1329,13 @@ export const isArchwingMission = (node: IRegion): boolean => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string | undefined => {
 | 
					export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string | undefined => {
 | 
				
			||||||
    if (config.worldState?.nightwaveOverride) {
 | 
					    if (config.worldState?.nightwaveOverride) {
 | 
				
			||||||
        return config.worldState.nightwaveOverride;
 | 
					        if (config.worldState.nightwaveOverride in nightwaveTagToSeason) {
 | 
				
			||||||
 | 
					            return config.worldState.nightwaveOverride;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        logger.warn(`ignoring invalid config value for worldState.nightwaveOverride`, {
 | 
				
			||||||
 | 
					            value: config.worldState.nightwaveOverride,
 | 
				
			||||||
 | 
					            valid_values: Object.keys(nightwaveTagToSeason)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) {
 | 
					    if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) {
 | 
				
			||||||
        return "RadioLegionIntermission13Syndicate";
 | 
					        return "RadioLegionIntermission13Syndicate";
 | 
				
			||||||
 | 
				
			|||||||
@ -416,6 +416,20 @@
 | 
				
			|||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div class="col-lg-6">
 | 
				
			||||||
 | 
					                        <div class="card mb-3" style="height: 400px;">
 | 
				
			||||||
 | 
					                            <h5 class="card-header" data-loc="inventory_Boosters"></h5>
 | 
				
			||||||
 | 
					                            <div class="card-body overflow-auto">
 | 
				
			||||||
 | 
					                                <form class="input-group mb-3" onsubmit="doAcquireBoosters();return false;">
 | 
				
			||||||
 | 
					                                    <input class="form-control" id="acquire-type-Boosters" list="datalist-Boosters" />
 | 
				
			||||||
 | 
					                                    <button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
 | 
				
			||||||
 | 
					                                </form>
 | 
				
			||||||
 | 
					                                <table class="table table-hover w-100">
 | 
				
			||||||
 | 
					                                    <tbody id="Boosters-list"></tbody>
 | 
				
			||||||
 | 
					                                </table>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="card mb-3">
 | 
					                <div class="card mb-3">
 | 
				
			||||||
                    <h5 class="card-header" data-loc="general_bulkActions"></h5>
 | 
					                    <h5 class="card-header" data-loc="general_bulkActions"></h5>
 | 
				
			||||||
@ -722,6 +736,10 @@
 | 
				
			|||||||
                                        <label class="form-label" for="spoofMasteryRank" data-loc="cheats_spoofMasteryRank"></label>
 | 
					                                        <label class="form-label" for="spoofMasteryRank" data-loc="cheats_spoofMasteryRank"></label>
 | 
				
			||||||
                                        <input class="form-control" id="spoofMasteryRank" type="number" min="-1" max="65535" />
 | 
					                                        <input class="form-control" id="spoofMasteryRank" type="number" min="-1" max="65535" />
 | 
				
			||||||
                                    </div>
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                    <div class="form-group mt-2">
 | 
				
			||||||
 | 
					                                        <label class="form-label" for="nightwaveStandingMultiplier" data-loc="cheats_nightwaveStandingMultiplier"></label>
 | 
				
			||||||
 | 
					                                        <input class="form-control" id="nightwaveStandingMultiplier" type="number" min="1" max="1000000" value="1" />
 | 
				
			||||||
 | 
					                                    </div>
 | 
				
			||||||
                                    <button class="btn btn-primary mt-3" type="submit" data-loc="cheats_saveSettings"></button>
 | 
					                                    <button class="btn btn-primary mt-3" type="submit" data-loc="cheats_saveSettings"></button>
 | 
				
			||||||
                                </form>
 | 
					                                </form>
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
@ -804,6 +822,7 @@
 | 
				
			|||||||
    <datalist id="datalist-ModularParts-CATBROW_MUTAGEN"></datalist>
 | 
					    <datalist id="datalist-ModularParts-CATBROW_MUTAGEN"></datalist>
 | 
				
			||||||
    <datalist id="datalist-ModularParts-KUBROW_ANTIGEN"></datalist>
 | 
					    <datalist id="datalist-ModularParts-KUBROW_ANTIGEN"></datalist>
 | 
				
			||||||
    <datalist id="datalist-ModularParts-KUBROW_MUTAGEN"></datalist>
 | 
					    <datalist id="datalist-ModularParts-KUBROW_MUTAGEN"></datalist>
 | 
				
			||||||
 | 
					    <datalist id="datalist-Boosters"></datalist>
 | 
				
			||||||
    <script src="/webui/libs/jquery-3.6.0.min.js"></script>
 | 
					    <script src="/webui/libs/jquery-3.6.0.min.js"></script>
 | 
				
			||||||
    <script src="/webui/libs/whirlpool-js.min.js"></script>
 | 
					    <script src="/webui/libs/whirlpool-js.min.js"></script>
 | 
				
			||||||
    <script src="/webui/libs/single.js"></script>
 | 
					    <script src="/webui/libs/single.js"></script>
 | 
				
			||||||
 | 
				
			|||||||
@ -1011,6 +1011,63 @@ function updateInventory() {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            document.getElementById("changeSyndicate").value = data.SupportedSyndicate ?? "";
 | 
					            document.getElementById("changeSyndicate").value = data.SupportedSyndicate ?? "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            document.getElementById("Boosters-list").innerHTML = "";
 | 
				
			||||||
 | 
					            const now = Math.floor(Date.now() / 1000);
 | 
				
			||||||
 | 
					            data.Boosters.forEach(({ ItemType, ExpiryDate }) => {
 | 
				
			||||||
 | 
					                if (ExpiryDate < now) {
 | 
				
			||||||
 | 
					                    // Booster has expired, skip it
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                const tr = document.createElement("tr");
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    const td = document.createElement("td");
 | 
				
			||||||
 | 
					                    td.textContent = itemMap[ItemType]?.name ?? ItemType;
 | 
				
			||||||
 | 
					                    tr.appendChild(td);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    const td = document.createElement("td");
 | 
				
			||||||
 | 
					                    td.classList = "text-end text-nowrap";
 | 
				
			||||||
 | 
					                    const timeString = formatDatetime("%Y-%m-%d %H:%M:%s", ExpiryDate * 1000);
 | 
				
			||||||
 | 
					                    const inlineForm = document.createElement("form");
 | 
				
			||||||
 | 
					                    const input = document.createElement("input");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    inlineForm.style.display = "inline-block";
 | 
				
			||||||
 | 
					                    inlineForm.onsubmit = function (event) {
 | 
				
			||||||
 | 
					                        event.preventDefault();
 | 
				
			||||||
 | 
					                        doChangeBoosterExpiry(ItemType, input);
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    input.type = "datetime-local";
 | 
				
			||||||
 | 
					                    input.classList.add("form-control");
 | 
				
			||||||
 | 
					                    input.classList.add("form-control-sm");
 | 
				
			||||||
 | 
					                    input.value = timeString;
 | 
				
			||||||
 | 
					                    let changed = false;
 | 
				
			||||||
 | 
					                    input.onchange = function () {
 | 
				
			||||||
 | 
					                        changed = true;
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    input.onblur = function () {
 | 
				
			||||||
 | 
					                        if (changed) {
 | 
				
			||||||
 | 
					                            doChangeBoosterExpiry(ItemType, input);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    inlineForm.appendChild(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    td.appendChild(inlineForm);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    const removeButton = document.createElement("a");
 | 
				
			||||||
 | 
					                    removeButton.title = loc("code_remove");
 | 
				
			||||||
 | 
					                    removeButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
 | 
				
			||||||
 | 
					                    removeButton.href = "#";
 | 
				
			||||||
 | 
					                    removeButton.onclick = function (event) {
 | 
				
			||||||
 | 
					                        event.preventDefault();
 | 
				
			||||||
 | 
					                        setBooster(ItemType, 0);
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    td.appendChild(removeButton);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    tr.appendChild(td);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                document.getElementById("Boosters-list").appendChild(tr);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -2027,3 +2084,77 @@ function handleModularSelection(category) {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setBooster(ItemType, ExpiryDate, callback) {
 | 
				
			||||||
 | 
					    revalidateAuthz(() => {
 | 
				
			||||||
 | 
					        $.post({
 | 
				
			||||||
 | 
					            url: "/custom/setBooster?" + window.authz,
 | 
				
			||||||
 | 
					            contentType: "application/json",
 | 
				
			||||||
 | 
					            data: JSON.stringify([
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    ItemType,
 | 
				
			||||||
 | 
					                    ExpiryDate
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ])
 | 
				
			||||||
 | 
					        }).done(function () {
 | 
				
			||||||
 | 
					            updateInventory();
 | 
				
			||||||
 | 
					            if (callback) callback();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function doAcquireBoosters() {
 | 
				
			||||||
 | 
					    const uniqueName = getKey(document.getElementById("acquire-type-Boosters"));
 | 
				
			||||||
 | 
					    if (!uniqueName) {
 | 
				
			||||||
 | 
					        $("#acquire-type-Boosters").addClass("is-invalid").focus();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const ExpiryDate = Date.now() / 1000 + 3 * 24 * 60 * 60; // default 3 days
 | 
				
			||||||
 | 
					    setBooster(uniqueName, ExpiryDate, () => {
 | 
				
			||||||
 | 
					        $("#acquire-type-Boosters").val("");
 | 
				
			||||||
 | 
					        updateInventory();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function doChangeBoosterExpiry(ItemType, ExpiryDateInput) {
 | 
				
			||||||
 | 
					    console.log("Changing booster expiry for", ItemType, "to", ExpiryDateInput.value);
 | 
				
			||||||
 | 
					    // cast local datetime string to unix timestamp
 | 
				
			||||||
 | 
					    const ExpiryDate = new Date(ExpiryDateInput.value).getTime() / 1000;
 | 
				
			||||||
 | 
					    if (isNaN(ExpiryDate)) {
 | 
				
			||||||
 | 
					        ExpiryDateInput.addClass("is-invalid").focus();
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setBooster(ItemType, ExpiryDate);
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatDatetime(fmt, date) {
 | 
				
			||||||
 | 
					    if (typeof date === "number") date = new Date(date);
 | 
				
			||||||
 | 
					    return fmt.replace(/(%[yY]|%m|%[Dd]|%H|%h|%M|%[Ss]|%[Pp])/g, match => {
 | 
				
			||||||
 | 
					        switch (match) {
 | 
				
			||||||
 | 
					            case "%Y":
 | 
				
			||||||
 | 
					                return date.getFullYear().toString();
 | 
				
			||||||
 | 
					            case "%y":
 | 
				
			||||||
 | 
					                return date.getFullYear().toString().slice(-2);
 | 
				
			||||||
 | 
					            case "%m":
 | 
				
			||||||
 | 
					                return (date.getMonth() + 1).toString().padStart(2, "0");
 | 
				
			||||||
 | 
					            case "%D":
 | 
				
			||||||
 | 
					            case "%d":
 | 
				
			||||||
 | 
					                return date.getDate().toString().padStart(2, "0");
 | 
				
			||||||
 | 
					            case "%H":
 | 
				
			||||||
 | 
					                return date.getHours().toString().padStart(2, "0");
 | 
				
			||||||
 | 
					            case "%h":
 | 
				
			||||||
 | 
					                return (date.getHours() % 12).toString().padStart(2, "0");
 | 
				
			||||||
 | 
					            case "%M":
 | 
				
			||||||
 | 
					                return date.getMinutes().toString().padStart(2, "0");
 | 
				
			||||||
 | 
					            case "%S":
 | 
				
			||||||
 | 
					            case "%s":
 | 
				
			||||||
 | 
					                return date.getSeconds().toString().padStart(2, "0");
 | 
				
			||||||
 | 
					            case "%P":
 | 
				
			||||||
 | 
					            case "%p":
 | 
				
			||||||
 | 
					                return date.getHours() < 12 ? "am" : "pm";
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                return match;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -98,6 +98,7 @@ dict = {
 | 
				
			|||||||
    inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`,
 | 
					    inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`,
 | 
				
			||||||
    inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`,
 | 
					    inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`,
 | 
				
			||||||
    inventory_bulkRankUpEvolutionProgress: `Alle Incarnon-Entwicklungsfortschritte auf Max. Rang`,
 | 
					    inventory_bulkRankUpEvolutionProgress: `Alle Incarnon-Entwicklungsfortschritte auf Max. Rang`,
 | 
				
			||||||
 | 
					    inventory_Boosters: `[UNTRANSLATED] Boosters`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    quests_list: `Quests`,
 | 
					    quests_list: `Quests`,
 | 
				
			||||||
    quests_completeAll: `Alle Quests abschließen`,
 | 
					    quests_completeAll: `Alle Quests abschließen`,
 | 
				
			||||||
@ -163,6 +164,7 @@ dict = {
 | 
				
			|||||||
    cheats_noDojoResearchTime: `Keine Dojo-Forschungszeit`,
 | 
					    cheats_noDojoResearchTime: `Keine Dojo-Forschungszeit`,
 | 
				
			||||||
    cheats_fastClanAscension: `Schneller Clan-Aufstieg`,
 | 
					    cheats_fastClanAscension: `Schneller Clan-Aufstieg`,
 | 
				
			||||||
    cheats_spoofMasteryRank: `Gefälschter Meisterschaftsrang (-1 zum deaktivieren)`,
 | 
					    cheats_spoofMasteryRank: `Gefälschter Meisterschaftsrang (-1 zum deaktivieren)`,
 | 
				
			||||||
 | 
					    cheats_nightwaveStandingMultiplier: `[UNTRANSLATED] Nightwave Standing Multiplier`,
 | 
				
			||||||
    cheats_saveSettings: `Einstellungen speichern`,
 | 
					    cheats_saveSettings: `Einstellungen speichern`,
 | 
				
			||||||
    cheats_account: `Account`,
 | 
					    cheats_account: `Account`,
 | 
				
			||||||
    cheats_unlockAllFocusSchools: `Alle Fokus-Schulen freischalten`,
 | 
					    cheats_unlockAllFocusSchools: `Alle Fokus-Schulen freischalten`,
 | 
				
			||||||
 | 
				
			|||||||
@ -97,6 +97,7 @@ dict = {
 | 
				
			|||||||
    inventory_bulkRankUpSentinels: `Max Rank All Sentinels`,
 | 
					    inventory_bulkRankUpSentinels: `Max Rank All Sentinels`,
 | 
				
			||||||
    inventory_bulkRankUpSentinelWeapons: `Max Rank All Sentinel Weapons`,
 | 
					    inventory_bulkRankUpSentinelWeapons: `Max Rank All Sentinel Weapons`,
 | 
				
			||||||
    inventory_bulkRankUpEvolutionProgress: `Max Rank All Incarnon Evolution Progress`,
 | 
					    inventory_bulkRankUpEvolutionProgress: `Max Rank All Incarnon Evolution Progress`,
 | 
				
			||||||
 | 
					    inventory_Boosters: `Boosters`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    quests_list: `Quests`,
 | 
					    quests_list: `Quests`,
 | 
				
			||||||
    quests_completeAll: `Complete All Quests`,
 | 
					    quests_completeAll: `Complete All Quests`,
 | 
				
			||||||
@ -162,6 +163,7 @@ dict = {
 | 
				
			|||||||
    cheats_noDojoResearchTime: `No Dojo Research Time`,
 | 
					    cheats_noDojoResearchTime: `No Dojo Research Time`,
 | 
				
			||||||
    cheats_fastClanAscension: `Fast Clan Ascension`,
 | 
					    cheats_fastClanAscension: `Fast Clan Ascension`,
 | 
				
			||||||
    cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`,
 | 
					    cheats_spoofMasteryRank: `Spoofed Mastery Rank (-1 to disable)`,
 | 
				
			||||||
 | 
					    cheats_nightwaveStandingMultiplier: `Nightwave Standing Multiplier`,
 | 
				
			||||||
    cheats_saveSettings: `Save Settings`,
 | 
					    cheats_saveSettings: `Save Settings`,
 | 
				
			||||||
    cheats_account: `Account`,
 | 
					    cheats_account: `Account`,
 | 
				
			||||||
    cheats_unlockAllFocusSchools: `Unlock All Focus Schools`,
 | 
					    cheats_unlockAllFocusSchools: `Unlock All Focus Schools`,
 | 
				
			||||||
 | 
				
			|||||||
@ -98,6 +98,7 @@ dict = {
 | 
				
			|||||||
    inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`,
 | 
					    inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`,
 | 
				
			||||||
    inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`,
 | 
					    inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`,
 | 
				
			||||||
    inventory_bulkRankUpEvolutionProgress: `Maximizar todo el progreso de evolución Incarnon`,
 | 
					    inventory_bulkRankUpEvolutionProgress: `Maximizar todo el progreso de evolución Incarnon`,
 | 
				
			||||||
 | 
					    inventory_Boosters: `Potenciadores`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    quests_list: `Misiones`,
 | 
					    quests_list: `Misiones`,
 | 
				
			||||||
    quests_completeAll: `Completar todas las misiones`,
 | 
					    quests_completeAll: `Completar todas las misiones`,
 | 
				
			||||||
@ -163,6 +164,7 @@ 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_spoofMasteryRank: `Rango de maestría simulado (-1 para desactivar)`,
 | 
					    cheats_spoofMasteryRank: `Rango de maestría simulado (-1 para desactivar)`,
 | 
				
			||||||
 | 
					    cheats_nightwaveStandingMultiplier: `Multiplicador de Reputación de Onda Nocturna`,
 | 
				
			||||||
    cheats_saveSettings: `Guardar configuración`,
 | 
					    cheats_saveSettings: `Guardar configuración`,
 | 
				
			||||||
    cheats_account: `Cuenta`,
 | 
					    cheats_account: `Cuenta`,
 | 
				
			||||||
    cheats_unlockAllFocusSchools: `Desbloquear todas las escuelas de enfoque`,
 | 
					    cheats_unlockAllFocusSchools: `Desbloquear todas las escuelas de enfoque`,
 | 
				
			||||||
 | 
				
			|||||||
@ -98,6 +98,7 @@ dict = {
 | 
				
			|||||||
    inventory_bulkRankUpSentinels: `Toutes les Sentinelles au rang max`,
 | 
					    inventory_bulkRankUpSentinels: `Toutes les Sentinelles au rang max`,
 | 
				
			||||||
    inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles au rang max`,
 | 
					    inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles au rang max`,
 | 
				
			||||||
    inventory_bulkRankUpEvolutionProgress: `Toutes les évolutions Incarnon au rang max`,
 | 
					    inventory_bulkRankUpEvolutionProgress: `Toutes les évolutions Incarnon au rang max`,
 | 
				
			||||||
 | 
					    inventory_Boosters: `[UNTRANSLATED] Boosters`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    quests_list: `Quêtes`,
 | 
					    quests_list: `Quêtes`,
 | 
				
			||||||
    quests_completeAll: `Compléter toutes les quêtes`,
 | 
					    quests_completeAll: `Compléter toutes les quêtes`,
 | 
				
			||||||
@ -163,6 +164,7 @@ dict = {
 | 
				
			|||||||
    cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`,
 | 
					    cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`,
 | 
				
			||||||
    cheats_fastClanAscension: `Ascension de clan rapide`,
 | 
					    cheats_fastClanAscension: `Ascension de clan rapide`,
 | 
				
			||||||
    cheats_spoofMasteryRank: `Rang de maîtrise personnalisé (-1 pour désactiver)`,
 | 
					    cheats_spoofMasteryRank: `Rang de maîtrise personnalisé (-1 pour désactiver)`,
 | 
				
			||||||
 | 
					    cheats_nightwaveStandingMultiplier: `[UNTRANSLATED] Nightwave Standing Multiplier`,
 | 
				
			||||||
    cheats_saveSettings: `Sauvegarder les paramètres`,
 | 
					    cheats_saveSettings: `Sauvegarder les paramètres`,
 | 
				
			||||||
    cheats_account: `Compte`,
 | 
					    cheats_account: `Compte`,
 | 
				
			||||||
    cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`,
 | 
					    cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`,
 | 
				
			||||||
 | 
				
			|||||||
@ -98,6 +98,7 @@ dict = {
 | 
				
			|||||||
    inventory_bulkRankUpSentinels: `Максимальный ранг всех стражей`,
 | 
					    inventory_bulkRankUpSentinels: `Максимальный ранг всех стражей`,
 | 
				
			||||||
    inventory_bulkRankUpSentinelWeapons: `Максимальный ранг всего оружия стражей`,
 | 
					    inventory_bulkRankUpSentinelWeapons: `Максимальный ранг всего оружия стражей`,
 | 
				
			||||||
    inventory_bulkRankUpEvolutionProgress: `Максимальный ранг всех эволюций Инкарнонов`,
 | 
					    inventory_bulkRankUpEvolutionProgress: `Максимальный ранг всех эволюций Инкарнонов`,
 | 
				
			||||||
 | 
					    inventory_Boosters: `[UNTRANSLATED] Boosters`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    quests_list: `Квесты`,
 | 
					    quests_list: `Квесты`,
 | 
				
			||||||
    quests_completeAll: `Завершить все квесты`,
 | 
					    quests_completeAll: `Завершить все квесты`,
 | 
				
			||||||
@ -163,6 +164,7 @@ dict = {
 | 
				
			|||||||
    cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`,
 | 
					    cheats_noDojoResearchTime: `Мгновенные Исследование Додзё`,
 | 
				
			||||||
    cheats_fastClanAscension: `Мгновенное Вознесение Клана`,
 | 
					    cheats_fastClanAscension: `Мгновенное Вознесение Клана`,
 | 
				
			||||||
    cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`,
 | 
					    cheats_spoofMasteryRank: `Подделанный ранг мастерства (-1 для отключения)`,
 | 
				
			||||||
 | 
					    cheats_nightwaveStandingMultiplier: `[UNTRANSLATED] Nightwave Standing Multiplier`,
 | 
				
			||||||
    cheats_saveSettings: `Сохранить настройки`,
 | 
					    cheats_saveSettings: `Сохранить настройки`,
 | 
				
			||||||
    cheats_account: `Аккаунт`,
 | 
					    cheats_account: `Аккаунт`,
 | 
				
			||||||
    cheats_unlockAllFocusSchools: `Разблокировать все школы фокуса`,
 | 
					    cheats_unlockAllFocusSchools: `Разблокировать все школы фокуса`,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
// Chinese translation by meb154
 | 
					// Chinese translation by meb154 & bishan178
 | 
				
			||||||
dict = {
 | 
					dict = {
 | 
				
			||||||
    general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`,
 | 
					    general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`,
 | 
				
			||||||
    general_addButton: `添加`,
 | 
					    general_addButton: `添加`,
 | 
				
			||||||
@ -18,7 +18,7 @@ dict = {
 | 
				
			|||||||
    code_kDrive: `K式悬浮板`,
 | 
					    code_kDrive: `K式悬浮板`,
 | 
				
			||||||
    code_legendaryCore: `传奇核心`,
 | 
					    code_legendaryCore: `传奇核心`,
 | 
				
			||||||
    code_traumaticPeculiar: `创伤怪奇`,
 | 
					    code_traumaticPeculiar: `创伤怪奇`,
 | 
				
			||||||
    code_starter: `|MOD| (有瑕疵的)`,
 | 
					    code_starter: `|MOD|(有瑕疵的)`,
 | 
				
			||||||
    code_badItem: `(Imposter)`,
 | 
					    code_badItem: `(Imposter)`,
 | 
				
			||||||
    code_maxRank: `满级`,
 | 
					    code_maxRank: `满级`,
 | 
				
			||||||
    code_rename: `重命名`,
 | 
					    code_rename: `重命名`,
 | 
				
			||||||
@ -28,7 +28,7 @@ dict = {
 | 
				
			|||||||
    code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`,
 | 
					    code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`,
 | 
				
			||||||
    code_noEquipmentToRankUp: `没有可升级的装备。`,
 | 
					    code_noEquipmentToRankUp: `没有可升级的装备。`,
 | 
				
			||||||
    code_succAdded: `已成功添加。`,
 | 
					    code_succAdded: `已成功添加。`,
 | 
				
			||||||
    code_succRemoved: `[UNTRANSLATED] Successfully removed.`,
 | 
					    code_succRemoved: `已成功移除。`,
 | 
				
			||||||
    code_buffsNumber: `增益数量`,
 | 
					    code_buffsNumber: `增益数量`,
 | 
				
			||||||
    code_cursesNumber: `负面数量`,
 | 
					    code_cursesNumber: `负面数量`,
 | 
				
			||||||
    code_rerollsNumber: `洗卡次数`,
 | 
					    code_rerollsNumber: `洗卡次数`,
 | 
				
			||||||
@ -39,27 +39,27 @@ dict = {
 | 
				
			|||||||
    code_count: `数量`,
 | 
					    code_count: `数量`,
 | 
				
			||||||
    code_focusAllUnlocked: `所有专精学派均已解锁。`,
 | 
					    code_focusAllUnlocked: `所有专精学派均已解锁。`,
 | 
				
			||||||
    code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`,
 | 
					    code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`,
 | 
				
			||||||
    code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`,
 | 
					    code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`,
 | 
				
			||||||
    code_succImport: `导入成功。`,
 | 
					    code_succImport: `导入成功。`,
 | 
				
			||||||
    code_gild: `镀金`,
 | 
					    code_gild: `镀金`,
 | 
				
			||||||
    code_moa: `恐鸟`,
 | 
					    code_moa: `恐鸟`,
 | 
				
			||||||
    code_zanuka: `猎犬`,
 | 
					    code_zanuka: `猎犬`,
 | 
				
			||||||
    code_stage: `[UNTRANSLATED] Stage`,
 | 
					    code_stage: `阶段`,
 | 
				
			||||||
    code_complete: `[UNTRANSLATED] Complete`,
 | 
					    code_complete: `完成`,
 | 
				
			||||||
    code_nextStage: `[UNTRANSLATED] Next stage`,
 | 
					    code_nextStage: `下一阶段`,
 | 
				
			||||||
    code_prevStage: `[UNTRANSLATED] Previous stage`,
 | 
					    code_prevStage: `上一阶段`,
 | 
				
			||||||
    code_reset: `[UNTRANSLATED] Reset`,
 | 
					    code_reset: `重置`,
 | 
				
			||||||
    code_setInactive: `[UNTRANSLATED] Make the quest inactive`,
 | 
					    code_setInactive: `使任务处于未激活状态`,
 | 
				
			||||||
    code_completed: `[UNTRANSLATED] Completed`,
 | 
					    code_completed: `已完成`,
 | 
				
			||||||
    code_active: `[UNTRANSLATED] Active`,
 | 
					    code_active: `正在执行`,
 | 
				
			||||||
    code_pigment: `颜料`,
 | 
					    code_pigment: `颜料`,
 | 
				
			||||||
    code_mature: `[UNTRANSLATED] Mature for combat`,
 | 
					    code_mature: `成长并战备`,
 | 
				
			||||||
    code_unmature: `[UNTRANSLATED] Regress genetic aging`,
 | 
					    code_unmature: `逆转衰老基因`,
 | 
				
			||||||
    login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`,
 | 
					    login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`,
 | 
				
			||||||
    login_emailLabel: `电子邮箱`,
 | 
					    login_emailLabel: `电子邮箱`,
 | 
				
			||||||
    login_passwordLabel: `密码`,
 | 
					    login_passwordLabel: `密码`,
 | 
				
			||||||
    login_loginButton: `登录`,
 | 
					    login_loginButton: `登录`,
 | 
				
			||||||
    login_registerButton: `[UNTRANSLATED] Register`,
 | 
					    login_registerButton: `注册账号`,
 | 
				
			||||||
    navbar_logout: `退出登录`,
 | 
					    navbar_logout: `退出登录`,
 | 
				
			||||||
    navbar_renameAccount: `重命名账户`,
 | 
					    navbar_renameAccount: `重命名账户`,
 | 
				
			||||||
    navbar_deleteAccount: `删除账户`,
 | 
					    navbar_deleteAccount: `删除账户`,
 | 
				
			||||||
@ -82,22 +82,23 @@ dict = {
 | 
				
			|||||||
    inventory_operatorAmps: `增幅器`,
 | 
					    inventory_operatorAmps: `增幅器`,
 | 
				
			||||||
    inventory_hoverboards: `K式悬浮板`,
 | 
					    inventory_hoverboards: `K式悬浮板`,
 | 
				
			||||||
    inventory_moaPets: `恐鸟`,
 | 
					    inventory_moaPets: `恐鸟`,
 | 
				
			||||||
    inventory_kubrowPets: `[UNTRANSLATED] Beasts`,
 | 
					    inventory_kubrowPets: `动物同伴`,
 | 
				
			||||||
    inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`,
 | 
					    inventory_evolutionProgress: `灵化之源进度`,
 | 
				
			||||||
    inventory_bulkAddSuits: `添加缺失战甲`,
 | 
					    inventory_bulkAddSuits: `添加缺失战甲`,
 | 
				
			||||||
    inventory_bulkAddWeapons: `添加缺失武器`,
 | 
					    inventory_bulkAddWeapons: `添加缺失武器`,
 | 
				
			||||||
    inventory_bulkAddSpaceSuits: `添加缺失Archwing`,
 | 
					    inventory_bulkAddSpaceSuits: `添加缺失Archwing`,
 | 
				
			||||||
    inventory_bulkAddSpaceWeapons: `添加缺失Archwing武器`,
 | 
					    inventory_bulkAddSpaceWeapons: `添加缺失Archwing武器`,
 | 
				
			||||||
    inventory_bulkAddSentinels: `添加缺失守护`,
 | 
					    inventory_bulkAddSentinels: `添加缺失守护`,
 | 
				
			||||||
    inventory_bulkAddSentinelWeapons: `添加缺失守护武器`,
 | 
					    inventory_bulkAddSentinelWeapons: `添加缺失守护武器`,
 | 
				
			||||||
    inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`,
 | 
					    inventory_bulkAddEvolutionProgress: `添加缺失的灵化之源`,
 | 
				
			||||||
    inventory_bulkRankUpSuits: `所有战甲升满级`,
 | 
					    inventory_bulkRankUpSuits: `所有战甲升满级`,
 | 
				
			||||||
    inventory_bulkRankUpWeapons: `所有武器升满级`,
 | 
					    inventory_bulkRankUpWeapons: `所有武器升满级`,
 | 
				
			||||||
    inventory_bulkRankUpSpaceSuits: `所有Archwing升满级`,
 | 
					    inventory_bulkRankUpSpaceSuits: `所有Archwing升满级`,
 | 
				
			||||||
    inventory_bulkRankUpSpaceWeapons: `所有Archwing武器升满级`,
 | 
					    inventory_bulkRankUpSpaceWeapons: `所有Archwing武器升满级`,
 | 
				
			||||||
    inventory_bulkRankUpSentinels: `所有守护升满级`,
 | 
					    inventory_bulkRankUpSentinels: `所有守护升满级`,
 | 
				
			||||||
    inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`,
 | 
					    inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`,
 | 
				
			||||||
    inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`,
 | 
					    inventory_bulkRankUpEvolutionProgress: `所有灵化之源最大等级`,
 | 
				
			||||||
 | 
					    inventory_Boosters: `加成器`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    quests_list: `任务`,
 | 
					    quests_list: `任务`,
 | 
				
			||||||
    quests_completeAll: `完成所有任务`,
 | 
					    quests_completeAll: `完成所有任务`,
 | 
				
			||||||
@ -111,15 +112,15 @@ dict = {
 | 
				
			|||||||
    currency_owned: `当前拥有 |COUNT|。`,
 | 
					    currency_owned: `当前拥有 |COUNT|。`,
 | 
				
			||||||
    powersuit_archonShardsLabel: `执刑官源力石槽位`,
 | 
					    powersuit_archonShardsLabel: `执刑官源力石槽位`,
 | 
				
			||||||
    powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`,
 | 
					    powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`,
 | 
				
			||||||
    powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`,
 | 
					    powersuit_archonShardsDescription2: `请注意, 在加载时, 每个执政官源力石都需要一定的时间来生效。`,
 | 
				
			||||||
    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_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`,
 | 
					    mods_addMissingUnrankedMods: `添加所有缺失的Mods`,
 | 
				
			||||||
    mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`,
 | 
					    mods_removeUnranked: `删除所有未升级的Mods`,
 | 
				
			||||||
    mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`,
 | 
					    mods_addMissingMaxRankMods: `添加所有缺失的满级Mods`,
 | 
				
			||||||
    cheats_administratorRequirement: `您必须是管理员才能使用此功能。要成为管理员,请将 <code>|DISPLAYNAME|</code> 添加到 config.json 的 <code>administratorNames</code> 中。`,
 | 
					    cheats_administratorRequirement: `您必须是管理员才能使用此功能。要成为管理员,请将 <code>|DISPLAYNAME|</code> 添加到 config.json 的 <code>administratorNames</code> 中。`,
 | 
				
			||||||
    cheats_server: `服务器`,
 | 
					    cheats_server: `服务器`,
 | 
				
			||||||
    cheats_skipTutorial: `跳过教程`,
 | 
					    cheats_skipTutorial: `跳过教程`,
 | 
				
			||||||
@ -131,43 +132,44 @@ dict = {
 | 
				
			|||||||
    cheats_infiniteEndo: `无限内融核心`,
 | 
					    cheats_infiniteEndo: `无限内融核心`,
 | 
				
			||||||
    cheats_infiniteRegalAya: `无限御品阿耶`,
 | 
					    cheats_infiniteRegalAya: `无限御品阿耶`,
 | 
				
			||||||
    cheats_infiniteHelminthMaterials: `无限Helminth材料`,
 | 
					    cheats_infiniteHelminthMaterials: `无限Helminth材料`,
 | 
				
			||||||
    cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`,
 | 
					    cheats_claimingBlueprintRefundsIngredients: `取消蓝图制造时返还材料`,
 | 
				
			||||||
    cheats_dontSubtractVoidTraces: `[UNTRANSLATED] Don't Subtract Void Traces`,
 | 
					    cheats_dontSubtractVoidTraces: `虚空光体无消耗`,
 | 
				
			||||||
    cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`,
 | 
					    cheats_dontSubtractConsumables: `消耗物品使用时无损耗`,
 | 
				
			||||||
    cheats_unlockAllShipFeatures: `解锁所有飞船功能`,
 | 
					    cheats_unlockAllShipFeatures: `解锁所有飞船功能`,
 | 
				
			||||||
    cheats_unlockAllShipDecorations: `解锁所有飞船装饰`,
 | 
					    cheats_unlockAllShipDecorations: `解锁所有飞船装饰`,
 | 
				
			||||||
    cheats_unlockAllFlavourItems: `解锁所有<abbr title=\"动画组合、图标、调色板等\">装饰物品</abbr>`,
 | 
					    cheats_unlockAllFlavourItems: `解锁所有<abbr title=\"动画组合、图标、调色板等\">装饰物品</abbr>`,
 | 
				
			||||||
    cheats_unlockAllSkins: `解锁所有外观`,
 | 
					    cheats_unlockAllSkins: `解锁所有外观`,
 | 
				
			||||||
    cheats_unlockAllCapturaScenes: `解锁所有Captura场景`,
 | 
					    cheats_unlockAllCapturaScenes: `解锁所有Captura场景`,
 | 
				
			||||||
    cheats_unlockAllDecoRecipes: `[UNTRANSLATED] Unlock All Dojo Deco Recipes`,
 | 
					    cheats_unlockAllDecoRecipes: `解锁所有道场配方`,
 | 
				
			||||||
    cheats_universalPolarityEverywhere: `全局万用极性`,
 | 
					    cheats_universalPolarityEverywhere: `全局万用极性`,
 | 
				
			||||||
    cheats_unlockDoubleCapacityPotatoesEverywhere: `全物品自带Orokin反应堆`,
 | 
					    cheats_unlockDoubleCapacityPotatoesEverywhere: `全物品自带Orokin反应堆`,
 | 
				
			||||||
    cheats_unlockExilusEverywhere: `全物品自带适配器`,
 | 
					    cheats_unlockExilusEverywhere: `全物品自带适配器`,
 | 
				
			||||||
    cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`,
 | 
					    cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`,
 | 
				
			||||||
    cheats_noDailyStandingLimits: `无每日声望限制`,
 | 
					    cheats_noDailyStandingLimits: `无每日声望限制`,
 | 
				
			||||||
    cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`,
 | 
					    cheats_noDailyFocusLimit: `指挥官专精无每日获取上限`,
 | 
				
			||||||
    cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`,
 | 
					    cheats_noArgonCrystalDecay: `氩结晶无衰变`,
 | 
				
			||||||
    cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`,
 | 
					    cheats_noMasteryRankUpCooldown: `段位考核无冷却时间`,
 | 
				
			||||||
    cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`,
 | 
					    cheats_noVendorPurchaseLimits: `商城或商人无购买限制`,
 | 
				
			||||||
    cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`,
 | 
					    cheats_noDeathMarks: `无死亡标记(不会被 Stalker/Grustrag 三霸/Zanuka 猎人等标记)`,
 | 
				
			||||||
    cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`,
 | 
					    cheats_noKimCooldowns: `无 KIM 冷却时间`,
 | 
				
			||||||
    cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`,
 | 
					    cheats_syndicateMissionsRepeatable: `集团任务可重复`,
 | 
				
			||||||
    cheats_instantFinishRivenChallenge: `[UNTRANSLATED] Instant Finish Riven Challenge`,
 | 
					    cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`,
 | 
				
			||||||
    cheats_instantResourceExtractorDrones: `即时资源采集无人机`,
 | 
					    cheats_instantResourceExtractorDrones: `即时资源采集无人机`,
 | 
				
			||||||
    cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`,
 | 
					    cheats_noResourceExtractorDronesDamage: `资源提取器不会损毁`,
 | 
				
			||||||
    cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`,
 | 
					    cheats_skipClanKeyCrafting: `跳过氏族钥匙制作, 进入道场无需氏族钥匙`,
 | 
				
			||||||
    cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`,
 | 
					    cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`,
 | 
				
			||||||
    cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`,
 | 
					    cheats_noDojoDecoBuildStage: `道场装饰建造立即完成`,
 | 
				
			||||||
    cheats_fastDojoRoomDestruction: `快速拆除道场房间`,
 | 
					    cheats_fastDojoRoomDestruction: `快速拆除道场房间`,
 | 
				
			||||||
    cheats_noDojoResearchCosts: `无视道场研究消耗`,
 | 
					    cheats_noDojoResearchCosts: `无视道场研究消耗`,
 | 
				
			||||||
    cheats_noDojoResearchTime: `无视道场研究时间`,
 | 
					    cheats_noDojoResearchTime: `无视道场研究时间`,
 | 
				
			||||||
    cheats_fastClanAscension: `快速升级氏族`,
 | 
					    cheats_fastClanAscension: `快速升级氏族`,
 | 
				
			||||||
    cheats_spoofMasteryRank: `伪造精通段位(-1为禁用)`,
 | 
					    cheats_spoofMasteryRank: `伪造精通段位(-1为禁用)`,
 | 
				
			||||||
 | 
					    cheats_nightwaveStandingMultiplier: `午夜电波声望倍率`,
 | 
				
			||||||
    cheats_saveSettings: `保存设置`,
 | 
					    cheats_saveSettings: `保存设置`,
 | 
				
			||||||
    cheats_account: `账户`,
 | 
					    cheats_account: `账户`,
 | 
				
			||||||
    cheats_unlockAllFocusSchools: `解锁所有专精学派`,
 | 
					    cheats_unlockAllFocusSchools: `解锁所有专精学派`,
 | 
				
			||||||
    cheats_helminthUnlockAll: `完全升级Helminth`,
 | 
					    cheats_helminthUnlockAll: `完全升级Helminth`,
 | 
				
			||||||
    cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`,
 | 
					    cheats_intrinsicsUnlockAll: `所有内源之力最大等级`,
 | 
				
			||||||
    cheats_changeSupportedSyndicate: `支持的集团`,
 | 
					    cheats_changeSupportedSyndicate: `支持的集团`,
 | 
				
			||||||
    cheats_changeButton: `更改`,
 | 
					    cheats_changeButton: `更改`,
 | 
				
			||||||
    cheats_none: `无`,
 | 
					    cheats_none: `无`,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user